@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.
@@ -270,6 +270,17 @@ function isCoordinatorName(agentName, employees = loadEmployeesSync()) {
270
270
  if (!agentName) return false;
271
271
  return agentName.toLowerCase() === getCoordinatorName(employees).toLowerCase();
272
272
  }
273
+ async function loadEmployees(employeesPath = EMPLOYEES_PATH) {
274
+ if (!existsSync3(employeesPath)) {
275
+ return [];
276
+ }
277
+ const raw = await readFile2(employeesPath, "utf-8");
278
+ try {
279
+ return JSON.parse(raw);
280
+ } catch {
281
+ return [];
282
+ }
283
+ }
273
284
  function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
274
285
  if (!existsSync3(employeesPath)) return [];
275
286
  try {
@@ -4037,8 +4048,35 @@ var init_tasks_crud = __esm({
4037
4048
  });
4038
4049
 
4039
4050
  // src/lib/tasks-review.ts
4051
+ var tasks_review_exports = {};
4052
+ __export(tasks_review_exports, {
4053
+ cleanupOrphanedReviews: () => cleanupOrphanedReviews,
4054
+ cleanupReviewFile: () => cleanupReviewFile,
4055
+ countNewPendingReviewsSince: () => countNewPendingReviewsSince,
4056
+ countPendingReviews: () => countPendingReviews,
4057
+ createReviewForCompletedTask: () => createReviewForCompletedTask,
4058
+ formatAge: () => formatAge,
4059
+ getReviewChecklist: () => getReviewChecklist,
4060
+ isStale: () => isStale,
4061
+ listPendingReviews: () => listPendingReviews
4062
+ });
4040
4063
  import path15 from "path";
4041
4064
  import { existsSync as existsSync13, readdirSync as readdirSync2, unlinkSync as unlinkSync4 } from "fs";
4065
+ function formatAge(isoTimestamp) {
4066
+ if (!isoTimestamp) return "";
4067
+ const ms = Date.now() - new Date(isoTimestamp).getTime();
4068
+ if (ms < 0) return "just now";
4069
+ const minutes = Math.floor(ms / 6e4);
4070
+ if (minutes < 60) return `${minutes}m ago`;
4071
+ const hours = Math.floor(minutes / 60);
4072
+ if (hours < 24) return `${hours}h ago`;
4073
+ const days = Math.floor(hours / 24);
4074
+ return `${days}d ago`;
4075
+ }
4076
+ function isStale(isoTimestamp) {
4077
+ if (!isoTimestamp) return false;
4078
+ return Date.now() - new Date(isoTimestamp).getTime() > 24 * 60 * 60 * 1e3;
4079
+ }
4042
4080
  async function countPendingReviews(sessionScope) {
4043
4081
  const client = getClient();
4044
4082
  const scope = strictSessionScopeFilter(
@@ -4159,6 +4197,95 @@ function getReviewChecklist(role, agent, taskSlug) {
4159
4197
  ]
4160
4198
  };
4161
4199
  }
4200
+ async function createReviewForCompletedTask(row, result, _baseDir, now) {
4201
+ const taskFile = String(row.task_file);
4202
+ const employees = await loadEmployees();
4203
+ const coordinatorName = getCoordinatorName(employees);
4204
+ if (isCoordinatorName(String(row.assigned_to), employees)) return;
4205
+ if (String(row.title).startsWith("Review:")) return;
4206
+ const fileName = taskFile.split("/").pop() ?? "";
4207
+ if (fileName.startsWith("review-") && String(row.assigned_by) === "system") return;
4208
+ if (fileName.startsWith("review-") && String(row.assigned_to) === "system") return;
4209
+ const client = getClient();
4210
+ const agent = String(row.assigned_to);
4211
+ const rawReviewer = row.reviewer || row.assigned_by;
4212
+ const reviewer = rawReviewer ? String(rawReviewer) : coordinatorName;
4213
+ const currentStatus = String(row.status ?? "");
4214
+ if (currentStatus === "done") return;
4215
+ const existingResult = String(row.result ?? "");
4216
+ if (existingResult.includes("## Review notes")) return;
4217
+ let reviewerRole = "unknown";
4218
+ try {
4219
+ const emp = getEmployee(employees, reviewer);
4220
+ if (emp) reviewerRole = emp.role;
4221
+ } catch {
4222
+ }
4223
+ const taskTitle = String(row.title);
4224
+ const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "unknown";
4225
+ const { lens, checklist } = getReviewChecklist(reviewerRole, agent, taskSlug);
4226
+ process.stderr.write(
4227
+ `[review] Annotating "${taskTitle}" for review by ${reviewer} (${reviewerRole})
4228
+ `
4229
+ );
4230
+ const reviewNotes = [
4231
+ `
4232
+ ---
4233
+ ## Review notes`,
4234
+ `Review lens: ${lens}`,
4235
+ `Reviewer: **${reviewer}** (${reviewerRole})`,
4236
+ "",
4237
+ "### Checklist",
4238
+ ...checklist,
4239
+ "",
4240
+ "### Verdict",
4241
+ "- **Approved:** mark this task as done",
4242
+ "- **Needs work:** re-open with notes"
4243
+ ].join("\n");
4244
+ const originalTaskId = String(row.id);
4245
+ const updatedResult = (result ?? "No result summary provided") + reviewNotes;
4246
+ await client.execute({
4247
+ sql: `UPDATE tasks SET result = ?, status = 'needs_review', updated_at = ?
4248
+ WHERE id = ?`,
4249
+ args: [updatedResult, now, originalTaskId]
4250
+ });
4251
+ orgBus.emit({
4252
+ type: "review_created",
4253
+ reviewId: originalTaskId,
4254
+ employee: agent,
4255
+ reviewer,
4256
+ timestamp: now
4257
+ });
4258
+ await writeNotification({
4259
+ agentId: agent,
4260
+ agentRole: String(row.assigned_to),
4261
+ event: "task_complete",
4262
+ project: String(row.project_name),
4263
+ summary: `completed "${taskTitle}" \u2014 ready for review`,
4264
+ taskFile
4265
+ });
4266
+ const originalPriority = String(row.priority).toLowerCase();
4267
+ const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
4268
+ if (!autoApprove) {
4269
+ try {
4270
+ const key = getSessionKey();
4271
+ const exeSession = getParentExe(key);
4272
+ if (exeSession) {
4273
+ sendIntercom(exeSession);
4274
+ }
4275
+ } catch {
4276
+ }
4277
+ }
4278
+ if (autoApprove) {
4279
+ process.stderr.write(
4280
+ `[review] Auto-approving "${taskTitle}" (P2 + tests pass)
4281
+ `
4282
+ );
4283
+ await client.execute({
4284
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ?",
4285
+ args: [now, originalTaskId]
4286
+ });
4287
+ }
4288
+ }
4162
4289
  async function cleanupReviewFile(row, taskFile, _baseDir) {
4163
4290
  if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
4164
4291
  try {
@@ -5584,15 +5711,17 @@ function sendIntercom(targetSession) {
5584
5711
  logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
5585
5712
  return "queued";
5586
5713
  }
5587
- try {
5588
- const rawAgent = targetSession.split("-")[0] ?? targetSession;
5589
- const agent = baseAgentName(rawAgent);
5590
- const markerPath = path18.join(SESSION_CACHE, `current-task-${agent}.json`);
5591
- if (existsSync14(markerPath)) {
5592
- logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker \u2014 will auto-chain)`);
5593
- return "debounced";
5714
+ if (sessionState !== "idle") {
5715
+ try {
5716
+ const rawAgent = targetSession.split("-")[0] ?? targetSession;
5717
+ const agent = baseAgentName(rawAgent);
5718
+ const markerPath = path18.join(SESSION_CACHE, `current-task-${agent}.json`);
5719
+ if (existsSync14(markerPath)) {
5720
+ logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
5721
+ return "debounced";
5722
+ }
5723
+ } catch {
5594
5724
  }
5595
- } catch {
5596
5725
  }
5597
5726
  try {
5598
5727
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
@@ -5663,6 +5792,24 @@ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitl
5663
5792
  try {
5664
5793
  const sessions = transport.listSessions();
5665
5794
  if (!sessions.includes(coordinatorSession)) return false;
5795
+ try {
5796
+ const { countPendingReviews: countPendingReviews2 } = (init_tasks_review(), __toCommonJS(tasks_review_exports));
5797
+ const pending = countPendingReviews2(coordinatorSession);
5798
+ if (pending instanceof Promise) {
5799
+ pending.then((count) => {
5800
+ if (count > 0) {
5801
+ execSync7(
5802
+ `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
5803
+ { timeout: 3e3 }
5804
+ );
5805
+ logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}", ${count} reviews pending)`);
5806
+ }
5807
+ }).catch(() => {
5808
+ });
5809
+ return true;
5810
+ }
5811
+ } catch {
5812
+ }
5666
5813
  execSync7(
5667
5814
  `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
5668
5815
  { timeout: 3e3 }
package/dist/tui/App.js CHANGED
@@ -4769,8 +4769,35 @@ var init_tasks_crud = __esm({
4769
4769
  });
4770
4770
 
4771
4771
  // src/lib/tasks-review.ts
4772
+ var tasks_review_exports = {};
4773
+ __export(tasks_review_exports, {
4774
+ cleanupOrphanedReviews: () => cleanupOrphanedReviews,
4775
+ cleanupReviewFile: () => cleanupReviewFile,
4776
+ countNewPendingReviewsSince: () => countNewPendingReviewsSince,
4777
+ countPendingReviews: () => countPendingReviews,
4778
+ createReviewForCompletedTask: () => createReviewForCompletedTask,
4779
+ formatAge: () => formatAge,
4780
+ getReviewChecklist: () => getReviewChecklist,
4781
+ isStale: () => isStale,
4782
+ listPendingReviews: () => listPendingReviews
4783
+ });
4772
4784
  import path14 from "path";
4773
4785
  import { existsSync as existsSync14, readdirSync as readdirSync2, unlinkSync as unlinkSync4 } from "fs";
4786
+ function formatAge(isoTimestamp) {
4787
+ if (!isoTimestamp) return "";
4788
+ const ms = Date.now() - new Date(isoTimestamp).getTime();
4789
+ if (ms < 0) return "just now";
4790
+ const minutes = Math.floor(ms / 6e4);
4791
+ if (minutes < 60) return `${minutes}m ago`;
4792
+ const hours = Math.floor(minutes / 60);
4793
+ if (hours < 24) return `${hours}h ago`;
4794
+ const days = Math.floor(hours / 24);
4795
+ return `${days}d ago`;
4796
+ }
4797
+ function isStale(isoTimestamp) {
4798
+ if (!isoTimestamp) return false;
4799
+ return Date.now() - new Date(isoTimestamp).getTime() > 24 * 60 * 60 * 1e3;
4800
+ }
4774
4801
  async function countPendingReviews(sessionScope) {
4775
4802
  const client = getClient();
4776
4803
  const scope = strictSessionScopeFilter(
@@ -4891,6 +4918,95 @@ function getReviewChecklist(role, agent, taskSlug) {
4891
4918
  ]
4892
4919
  };
4893
4920
  }
4921
+ async function createReviewForCompletedTask(row, result, _baseDir, now) {
4922
+ const taskFile = String(row.task_file);
4923
+ const employees = await loadEmployees();
4924
+ const coordinatorName = getCoordinatorName(employees);
4925
+ if (isCoordinatorName(String(row.assigned_to), employees)) return;
4926
+ if (String(row.title).startsWith("Review:")) return;
4927
+ const fileName = taskFile.split("/").pop() ?? "";
4928
+ if (fileName.startsWith("review-") && String(row.assigned_by) === "system") return;
4929
+ if (fileName.startsWith("review-") && String(row.assigned_to) === "system") return;
4930
+ const client = getClient();
4931
+ const agent = String(row.assigned_to);
4932
+ const rawReviewer = row.reviewer || row.assigned_by;
4933
+ const reviewer = rawReviewer ? String(rawReviewer) : coordinatorName;
4934
+ const currentStatus = String(row.status ?? "");
4935
+ if (currentStatus === "done") return;
4936
+ const existingResult = String(row.result ?? "");
4937
+ if (existingResult.includes("## Review notes")) return;
4938
+ let reviewerRole = "unknown";
4939
+ try {
4940
+ const emp = getEmployee(employees, reviewer);
4941
+ if (emp) reviewerRole = emp.role;
4942
+ } catch {
4943
+ }
4944
+ const taskTitle = String(row.title);
4945
+ const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "unknown";
4946
+ const { lens, checklist } = getReviewChecklist(reviewerRole, agent, taskSlug);
4947
+ process.stderr.write(
4948
+ `[review] Annotating "${taskTitle}" for review by ${reviewer} (${reviewerRole})
4949
+ `
4950
+ );
4951
+ const reviewNotes = [
4952
+ `
4953
+ ---
4954
+ ## Review notes`,
4955
+ `Review lens: ${lens}`,
4956
+ `Reviewer: **${reviewer}** (${reviewerRole})`,
4957
+ "",
4958
+ "### Checklist",
4959
+ ...checklist,
4960
+ "",
4961
+ "### Verdict",
4962
+ "- **Approved:** mark this task as done",
4963
+ "- **Needs work:** re-open with notes"
4964
+ ].join("\n");
4965
+ const originalTaskId = String(row.id);
4966
+ const updatedResult = (result ?? "No result summary provided") + reviewNotes;
4967
+ await client.execute({
4968
+ sql: `UPDATE tasks SET result = ?, status = 'needs_review', updated_at = ?
4969
+ WHERE id = ?`,
4970
+ args: [updatedResult, now, originalTaskId]
4971
+ });
4972
+ orgBus.emit({
4973
+ type: "review_created",
4974
+ reviewId: originalTaskId,
4975
+ employee: agent,
4976
+ reviewer,
4977
+ timestamp: now
4978
+ });
4979
+ await writeNotification({
4980
+ agentId: agent,
4981
+ agentRole: String(row.assigned_to),
4982
+ event: "task_complete",
4983
+ project: String(row.project_name),
4984
+ summary: `completed "${taskTitle}" \u2014 ready for review`,
4985
+ taskFile
4986
+ });
4987
+ const originalPriority = String(row.priority).toLowerCase();
4988
+ const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
4989
+ if (!autoApprove) {
4990
+ try {
4991
+ const key = getSessionKey();
4992
+ const exeSession = getParentExe(key);
4993
+ if (exeSession) {
4994
+ sendIntercom(exeSession);
4995
+ }
4996
+ } catch {
4997
+ }
4998
+ }
4999
+ if (autoApprove) {
5000
+ process.stderr.write(
5001
+ `[review] Auto-approving "${taskTitle}" (P2 + tests pass)
5002
+ `
5003
+ );
5004
+ await client.execute({
5005
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ?",
5006
+ args: [now, originalTaskId]
5007
+ });
5008
+ }
5009
+ }
4894
5010
  async function cleanupReviewFile(row, taskFile, _baseDir) {
4895
5011
  if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
4896
5012
  try {
@@ -6254,15 +6370,17 @@ function sendIntercom(targetSession) {
6254
6370
  logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
6255
6371
  return "queued";
6256
6372
  }
6257
- try {
6258
- const rawAgent = targetSession.split("-")[0] ?? targetSession;
6259
- const agent = baseAgentName(rawAgent);
6260
- const markerPath = path17.join(SESSION_CACHE, `current-task-${agent}.json`);
6261
- if (existsSync15(markerPath)) {
6262
- logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker \u2014 will auto-chain)`);
6263
- return "debounced";
6373
+ if (sessionState !== "idle") {
6374
+ try {
6375
+ const rawAgent = targetSession.split("-")[0] ?? targetSession;
6376
+ const agent = baseAgentName(rawAgent);
6377
+ const markerPath = path17.join(SESSION_CACHE, `current-task-${agent}.json`);
6378
+ if (existsSync15(markerPath)) {
6379
+ logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
6380
+ return "debounced";
6381
+ }
6382
+ } catch {
6264
6383
  }
6265
- } catch {
6266
6384
  }
6267
6385
  try {
6268
6386
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
@@ -6333,6 +6451,24 @@ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitl
6333
6451
  try {
6334
6452
  const sessions = transport.listSessions();
6335
6453
  if (!sessions.includes(coordinatorSession)) return false;
6454
+ try {
6455
+ const { countPendingReviews: countPendingReviews2 } = (init_tasks_review(), __toCommonJS(tasks_review_exports));
6456
+ const pending = countPendingReviews2(coordinatorSession);
6457
+ if (pending instanceof Promise) {
6458
+ pending.then((count) => {
6459
+ if (count > 0) {
6460
+ execSync7(
6461
+ `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
6462
+ { timeout: 3e3 }
6463
+ );
6464
+ logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}", ${count} reviews pending)`);
6465
+ }
6466
+ }).catch(() => {
6467
+ });
6468
+ return true;
6469
+ }
6470
+ } catch {
6471
+ }
6336
6472
  execSync7(
6337
6473
  `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
6338
6474
  { timeout: 3e3 }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askexenow/exe-os",
3
- "version": "0.9.10",
3
+ "version": "0.9.11",
4
4
  "description": "AI employee operating system — persistent memory, task management, and multi-agent coordination for Claude Code.",
5
5
  "license": "CC-BY-NC-4.0",
6
6
  "type": "module",