@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.
@@ -7545,6 +7545,314 @@ var init_capacity_monitor = __esm({
7545
7545
  }
7546
7546
  });
7547
7547
 
7548
+ // src/lib/tasks-review.ts
7549
+ var tasks_review_exports = {};
7550
+ __export(tasks_review_exports, {
7551
+ cleanupOrphanedReviews: () => cleanupOrphanedReviews,
7552
+ cleanupReviewFile: () => cleanupReviewFile,
7553
+ countNewPendingReviewsSince: () => countNewPendingReviewsSince,
7554
+ countPendingReviews: () => countPendingReviews,
7555
+ createReviewForCompletedTask: () => createReviewForCompletedTask,
7556
+ formatAge: () => formatAge,
7557
+ getReviewChecklist: () => getReviewChecklist,
7558
+ isStale: () => isStale,
7559
+ listPendingReviews: () => listPendingReviews
7560
+ });
7561
+ import path19 from "path";
7562
+ import { existsSync as existsSync15, readdirSync as readdirSync4, unlinkSync as unlinkSync4 } from "fs";
7563
+ function formatAge(isoTimestamp) {
7564
+ if (!isoTimestamp) return "";
7565
+ const ms = Date.now() - new Date(isoTimestamp).getTime();
7566
+ if (ms < 0) return "just now";
7567
+ const minutes = Math.floor(ms / 6e4);
7568
+ if (minutes < 60) return `${minutes}m ago`;
7569
+ const hours = Math.floor(minutes / 60);
7570
+ if (hours < 24) return `${hours}h ago`;
7571
+ const days = Math.floor(hours / 24);
7572
+ return `${days}d ago`;
7573
+ }
7574
+ function isStale(isoTimestamp) {
7575
+ if (!isoTimestamp) return false;
7576
+ return Date.now() - new Date(isoTimestamp).getTime() > 24 * 60 * 60 * 1e3;
7577
+ }
7578
+ async function countPendingReviews(sessionScope) {
7579
+ const client = getClient();
7580
+ const scope = strictSessionScopeFilter(
7581
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
7582
+ );
7583
+ const result = await client.execute({
7584
+ sql: `SELECT COUNT(*) as cnt FROM tasks
7585
+ WHERE status = 'needs_review'${scope.sql}`,
7586
+ args: [...scope.args]
7587
+ });
7588
+ return Number(result.rows[0]?.cnt) || 0;
7589
+ }
7590
+ async function countNewPendingReviewsSince(sinceIso, sessionScope) {
7591
+ const client = getClient();
7592
+ const scope = strictSessionScopeFilter(
7593
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
7594
+ );
7595
+ const result = await client.execute({
7596
+ sql: `SELECT COUNT(*) as cnt FROM tasks
7597
+ WHERE status = 'needs_review' AND updated_at > ?${scope.sql}`,
7598
+ args: [sinceIso, ...scope.args]
7599
+ });
7600
+ return Number(result.rows[0]?.cnt) || 0;
7601
+ }
7602
+ async function listPendingReviews(limit, sessionScope) {
7603
+ const client = getClient();
7604
+ const scope = strictSessionScopeFilter(
7605
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
7606
+ );
7607
+ const result = await client.execute({
7608
+ sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
7609
+ WHERE status = 'needs_review'${scope.sql}
7610
+ ORDER BY updated_at ASC LIMIT ?`,
7611
+ args: [...scope.args, limit]
7612
+ });
7613
+ return result.rows;
7614
+ }
7615
+ async function cleanupOrphanedReviews() {
7616
+ const client = getClient();
7617
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7618
+ const r1 = await client.execute({
7619
+ sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
7620
+ WHERE status IN ('open', 'needs_review', 'in_progress')
7621
+ AND assigned_by = 'system'
7622
+ AND title LIKE 'Review:%'
7623
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))`,
7624
+ args: [now]
7625
+ });
7626
+ const r1b = await client.execute({
7627
+ sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
7628
+ WHERE status IN ('open', 'needs_review')
7629
+ AND title LIKE 'Review:%completed%'
7630
+ AND (parent_task_id IS NULL OR parent_task_id NOT IN (SELECT id FROM tasks WHERE status IN ('open', 'in_progress', 'needs_review', 'blocked')))`,
7631
+ args: [now]
7632
+ });
7633
+ const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
7634
+ const r2 = await client.execute({
7635
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
7636
+ WHERE status = 'needs_review'
7637
+ AND result IS NOT NULL
7638
+ AND updated_at < ?`,
7639
+ args: [now, staleThreshold]
7640
+ });
7641
+ const total = r1.rowsAffected + (r1b?.rowsAffected ?? 0) + r2.rowsAffected;
7642
+ if (total > 0) {
7643
+ process.stderr.write(
7644
+ `[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r1b?.rowsAffected ?? 0} orphan + ${r2.rowsAffected} stale
7645
+ `
7646
+ );
7647
+ }
7648
+ return total;
7649
+ }
7650
+ function getReviewChecklist(role, agent, taskSlug) {
7651
+ const roleLower = role.toLowerCase();
7652
+ if (roleLower.includes("engineer") || roleLower === "principal engineer") {
7653
+ return {
7654
+ lens: "Code Quality (Engineer)",
7655
+ checklist: [
7656
+ "1. Do all tests pass? Any new tests needed?",
7657
+ "2. Is the code clean \u2014 no dead code, no TODOs left?",
7658
+ "3. Does it follow existing patterns and conventions in the codebase?",
7659
+ "4. Any regressions in the test suite?"
7660
+ ]
7661
+ };
7662
+ }
7663
+ if (roleLower === "cto" || roleLower.includes("architect")) {
7664
+ return {
7665
+ lens: "Architecture (CTO)",
7666
+ checklist: [
7667
+ "1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
7668
+ "2. Is it backward compatible? Any breaking changes?",
7669
+ "3. Does it introduce technical debt? Is that debt justified?",
7670
+ "4. Security implications? Any new attack surface?",
7671
+ "5. Does it scale? Performance considerations?",
7672
+ "6. Coordination: does this affect other employees' work or other projects?"
7673
+ ]
7674
+ };
7675
+ }
7676
+ if (roleLower === "coo" || roleLower.includes("operations")) {
7677
+ return {
7678
+ lens: "Strategic (COO)",
7679
+ checklist: [
7680
+ "1. Does this serve the project mission?",
7681
+ "2. Is this the right work at the right time?",
7682
+ "3. Does the architectural assessment make sense for the business?",
7683
+ "4. Any cross-project implications?"
7684
+ ]
7685
+ };
7686
+ }
7687
+ return {
7688
+ lens: "General",
7689
+ checklist: [
7690
+ "1. Read the original task's acceptance criteria",
7691
+ `2. Check git log for related commits: \`git log --oneline --author-date-order -10\``,
7692
+ "3. Verify code changes match requirements",
7693
+ "4. Check if tests were added/updated",
7694
+ `5. Look for output files in exe/output/${agent}-${taskSlug}*`
7695
+ ]
7696
+ };
7697
+ }
7698
+ async function createReviewForCompletedTask(row, result, _baseDir, now) {
7699
+ const taskFile = String(row.task_file);
7700
+ const employees = await loadEmployees();
7701
+ const coordinatorName = getCoordinatorName(employees);
7702
+ if (isCoordinatorName(String(row.assigned_to), employees)) return;
7703
+ if (String(row.title).startsWith("Review:")) return;
7704
+ const fileName = taskFile.split("/").pop() ?? "";
7705
+ if (fileName.startsWith("review-") && String(row.assigned_by) === "system") return;
7706
+ if (fileName.startsWith("review-") && String(row.assigned_to) === "system") return;
7707
+ const client = getClient();
7708
+ const agent = String(row.assigned_to);
7709
+ const rawReviewer = row.reviewer || row.assigned_by;
7710
+ const reviewer = rawReviewer ? String(rawReviewer) : coordinatorName;
7711
+ const currentStatus = String(row.status ?? "");
7712
+ if (currentStatus === "done") return;
7713
+ const existingResult = String(row.result ?? "");
7714
+ if (existingResult.includes("## Review notes")) return;
7715
+ let reviewerRole = "unknown";
7716
+ try {
7717
+ const emp = getEmployee(employees, reviewer);
7718
+ if (emp) reviewerRole = emp.role;
7719
+ } catch {
7720
+ }
7721
+ const taskTitle = String(row.title);
7722
+ const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "unknown";
7723
+ const { lens, checklist } = getReviewChecklist(reviewerRole, agent, taskSlug);
7724
+ process.stderr.write(
7725
+ `[review] Annotating "${taskTitle}" for review by ${reviewer} (${reviewerRole})
7726
+ `
7727
+ );
7728
+ const reviewNotes = [
7729
+ `
7730
+ ---
7731
+ ## Review notes`,
7732
+ `Review lens: ${lens}`,
7733
+ `Reviewer: **${reviewer}** (${reviewerRole})`,
7734
+ "",
7735
+ "### Checklist",
7736
+ ...checklist,
7737
+ "",
7738
+ "### Verdict",
7739
+ "- **Approved:** mark this task as done",
7740
+ "- **Needs work:** re-open with notes"
7741
+ ].join("\n");
7742
+ const originalTaskId = String(row.id);
7743
+ const updatedResult = (result ?? "No result summary provided") + reviewNotes;
7744
+ await client.execute({
7745
+ sql: `UPDATE tasks SET result = ?, status = 'needs_review', updated_at = ?
7746
+ WHERE id = ?`,
7747
+ args: [updatedResult, now, originalTaskId]
7748
+ });
7749
+ orgBus.emit({
7750
+ type: "review_created",
7751
+ reviewId: originalTaskId,
7752
+ employee: agent,
7753
+ reviewer,
7754
+ timestamp: now
7755
+ });
7756
+ await writeNotification({
7757
+ agentId: agent,
7758
+ agentRole: String(row.assigned_to),
7759
+ event: "task_complete",
7760
+ project: String(row.project_name),
7761
+ summary: `completed "${taskTitle}" \u2014 ready for review`,
7762
+ taskFile
7763
+ });
7764
+ const originalPriority = String(row.priority).toLowerCase();
7765
+ const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
7766
+ if (!autoApprove) {
7767
+ try {
7768
+ const key = getSessionKey();
7769
+ const exeSession = getParentExe(key);
7770
+ if (exeSession) {
7771
+ sendIntercom(exeSession);
7772
+ }
7773
+ } catch {
7774
+ }
7775
+ }
7776
+ if (autoApprove) {
7777
+ process.stderr.write(
7778
+ `[review] Auto-approving "${taskTitle}" (P2 + tests pass)
7779
+ `
7780
+ );
7781
+ await client.execute({
7782
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ?",
7783
+ args: [now, originalTaskId]
7784
+ });
7785
+ }
7786
+ }
7787
+ async function cleanupReviewFile(row, taskFile, _baseDir) {
7788
+ if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
7789
+ try {
7790
+ const client = getClient();
7791
+ const now = (/* @__PURE__ */ new Date()).toISOString();
7792
+ const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
7793
+ if (parentId) {
7794
+ const result = await client.execute({
7795
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
7796
+ args: [now, parentId]
7797
+ });
7798
+ if (result.rowsAffected > 0) {
7799
+ process.stderr.write(
7800
+ `[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
7801
+ `
7802
+ );
7803
+ }
7804
+ } else {
7805
+ const fileName = taskFile.split("/").pop() ?? "";
7806
+ const reviewPrefix = fileName.replace(".md", "");
7807
+ const parts = reviewPrefix.split("-");
7808
+ if (parts.length >= 3 && parts[0] === "review") {
7809
+ const agent = parts[1];
7810
+ const slug = parts.slice(2).join("-");
7811
+ const legacyTaskFile = `exe/${agent}/${slug}.md`;
7812
+ const result = await client.execute({
7813
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
7814
+ args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
7815
+ });
7816
+ if (result.rowsAffected > 0) {
7817
+ process.stderr.write(
7818
+ `[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
7819
+ `
7820
+ );
7821
+ }
7822
+ }
7823
+ }
7824
+ } catch (err) {
7825
+ process.stderr.write(
7826
+ `[review-cleanup] Failed to cascade original task: ${err instanceof Error ? err.message : String(err)}
7827
+ `
7828
+ );
7829
+ }
7830
+ try {
7831
+ const cacheDir = path19.join(EXE_AI_DIR, "session-cache");
7832
+ if (existsSync15(cacheDir)) {
7833
+ for (const f of readdirSync4(cacheDir)) {
7834
+ if (f.startsWith("review-notified-")) {
7835
+ unlinkSync4(path19.join(cacheDir, f));
7836
+ }
7837
+ }
7838
+ }
7839
+ } catch {
7840
+ }
7841
+ }
7842
+ var init_tasks_review = __esm({
7843
+ "src/lib/tasks-review.ts"() {
7844
+ "use strict";
7845
+ init_database();
7846
+ init_config();
7847
+ init_employees();
7848
+ init_notifications();
7849
+ init_tmux_routing();
7850
+ init_session_key();
7851
+ init_state_bus();
7852
+ init_task_scope();
7853
+ }
7854
+ });
7855
+
7548
7856
  // src/lib/tmux-routing.ts
7549
7857
  var tmux_routing_exports = {};
7550
7858
  __export(tmux_routing_exports, {
@@ -7571,13 +7879,13 @@ __export(tmux_routing_exports, {
7571
7879
  verifyPaneAtCapacity: () => verifyPaneAtCapacity
7572
7880
  });
7573
7881
  import { execFileSync as execFileSync2, execSync as execSync7 } from "child_process";
7574
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync10, mkdirSync as mkdirSync7, existsSync as existsSync15, appendFileSync, readdirSync as readdirSync4 } from "fs";
7575
- import path19 from "path";
7882
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync10, mkdirSync as mkdirSync7, existsSync as existsSync16, appendFileSync, readdirSync as readdirSync5 } from "fs";
7883
+ import path20 from "path";
7576
7884
  import os9 from "os";
7577
7885
  import { fileURLToPath as fileURLToPath2 } from "url";
7578
- import { unlinkSync as unlinkSync4 } from "fs";
7886
+ import { unlinkSync as unlinkSync5 } from "fs";
7579
7887
  function spawnLockPath(sessionName) {
7580
- return path19.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
7888
+ return path20.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
7581
7889
  }
7582
7890
  function isProcessAlive(pid) {
7583
7891
  try {
@@ -7588,11 +7896,11 @@ function isProcessAlive(pid) {
7588
7896
  }
7589
7897
  }
7590
7898
  function acquireSpawnLock2(sessionName) {
7591
- if (!existsSync15(SPAWN_LOCK_DIR)) {
7899
+ if (!existsSync16(SPAWN_LOCK_DIR)) {
7592
7900
  mkdirSync7(SPAWN_LOCK_DIR, { recursive: true });
7593
7901
  }
7594
7902
  const lockFile = spawnLockPath(sessionName);
7595
- if (existsSync15(lockFile)) {
7903
+ if (existsSync16(lockFile)) {
7596
7904
  try {
7597
7905
  const lock = JSON.parse(readFileSync12(lockFile, "utf8"));
7598
7906
  const age = Date.now() - lock.timestamp;
@@ -7607,20 +7915,20 @@ function acquireSpawnLock2(sessionName) {
7607
7915
  }
7608
7916
  function releaseSpawnLock2(sessionName) {
7609
7917
  try {
7610
- unlinkSync4(spawnLockPath(sessionName));
7918
+ unlinkSync5(spawnLockPath(sessionName));
7611
7919
  } catch {
7612
7920
  }
7613
7921
  }
7614
7922
  function resolveBehaviorsExporterScript() {
7615
7923
  try {
7616
7924
  const thisFile = fileURLToPath2(import.meta.url);
7617
- const scriptPath = path19.join(
7618
- path19.dirname(thisFile),
7925
+ const scriptPath = path20.join(
7926
+ path20.dirname(thisFile),
7619
7927
  "..",
7620
7928
  "bin",
7621
7929
  "exe-export-behaviors.js"
7622
7930
  );
7623
- return existsSync15(scriptPath) ? scriptPath : null;
7931
+ return existsSync16(scriptPath) ? scriptPath : null;
7624
7932
  } catch {
7625
7933
  return null;
7626
7934
  }
@@ -7686,11 +7994,11 @@ function extractRootExe(name) {
7686
7994
  return parts.length > 0 ? parts[parts.length - 1] : null;
7687
7995
  }
7688
7996
  function registerParentExe(sessionKey, parentExe, dispatchedBy) {
7689
- if (!existsSync15(SESSION_CACHE)) {
7997
+ if (!existsSync16(SESSION_CACHE)) {
7690
7998
  mkdirSync7(SESSION_CACHE, { recursive: true });
7691
7999
  }
7692
8000
  const rootExe = extractRootExe(parentExe) ?? parentExe;
7693
- const filePath = path19.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
8001
+ const filePath = path20.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
7694
8002
  writeFileSync10(filePath, JSON.stringify({
7695
8003
  parentExe: rootExe,
7696
8004
  dispatchedBy: dispatchedBy || rootExe,
@@ -7699,7 +8007,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
7699
8007
  }
7700
8008
  function getParentExe(sessionKey) {
7701
8009
  try {
7702
- const data = JSON.parse(readFileSync12(path19.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
8010
+ const data = JSON.parse(readFileSync12(path20.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
7703
8011
  return data.parentExe || null;
7704
8012
  } catch {
7705
8013
  return null;
@@ -7708,7 +8016,7 @@ function getParentExe(sessionKey) {
7708
8016
  function getDispatchedBy(sessionKey) {
7709
8017
  try {
7710
8018
  const data = JSON.parse(readFileSync12(
7711
- path19.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
8019
+ path20.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
7712
8020
  "utf8"
7713
8021
  ));
7714
8022
  return data.dispatchedBy ?? data.parentExe ?? null;
@@ -7778,7 +8086,7 @@ async function verifyPaneAtCapacity(sessionName) {
7778
8086
  }
7779
8087
  function readDebounceState() {
7780
8088
  try {
7781
- if (!existsSync15(DEBOUNCE_FILE)) return {};
8089
+ if (!existsSync16(DEBOUNCE_FILE)) return {};
7782
8090
  const raw = JSON.parse(readFileSync12(DEBOUNCE_FILE, "utf8"));
7783
8091
  const state = {};
7784
8092
  for (const [key, val] of Object.entries(raw)) {
@@ -7795,7 +8103,7 @@ function readDebounceState() {
7795
8103
  }
7796
8104
  function writeDebounceState(state) {
7797
8105
  try {
7798
- if (!existsSync15(SESSION_CACHE)) mkdirSync7(SESSION_CACHE, { recursive: true });
8106
+ if (!existsSync16(SESSION_CACHE)) mkdirSync7(SESSION_CACHE, { recursive: true });
7799
8107
  writeFileSync10(DEBOUNCE_FILE, JSON.stringify(state));
7800
8108
  } catch {
7801
8109
  }
@@ -7891,22 +8199,24 @@ function sendIntercom(targetSession) {
7891
8199
  logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
7892
8200
  return "queued";
7893
8201
  }
7894
- try {
7895
- const rawAgent = targetSession.split("-")[0] ?? targetSession;
7896
- const agent = baseAgentName(rawAgent);
7897
- const markerPath = path19.join(SESSION_CACHE, `current-task-${agent}.json`);
7898
- if (existsSync15(markerPath)) {
7899
- logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker \u2014 will auto-chain)`);
7900
- return "debounced";
8202
+ if (sessionState !== "idle") {
8203
+ try {
8204
+ const rawAgent = targetSession.split("-")[0] ?? targetSession;
8205
+ const agent = baseAgentName(rawAgent);
8206
+ const markerPath = path20.join(SESSION_CACHE, `current-task-${agent}.json`);
8207
+ if (existsSync16(markerPath)) {
8208
+ logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
8209
+ return "debounced";
8210
+ }
8211
+ } catch {
7901
8212
  }
7902
- } catch {
7903
8213
  }
7904
8214
  try {
7905
8215
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
7906
8216
  const agent = baseAgentName(rawAgent);
7907
- const taskDir = path19.join(process.cwd(), "exe", agent);
7908
- if (existsSync15(taskDir)) {
7909
- const files = readdirSync4(taskDir).filter(
8217
+ const taskDir = path20.join(process.cwd(), "exe", agent);
8218
+ if (existsSync16(taskDir)) {
8219
+ const files = readdirSync5(taskDir).filter(
7910
8220
  (f) => f.endsWith(".md") && f !== "DONE.txt"
7911
8221
  );
7912
8222
  if (files.length === 0) {
@@ -7970,6 +8280,24 @@ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitl
7970
8280
  try {
7971
8281
  const sessions = transport.listSessions();
7972
8282
  if (!sessions.includes(coordinatorSession)) return false;
8283
+ try {
8284
+ const { countPendingReviews: countPendingReviews2 } = (init_tasks_review(), __toCommonJS(tasks_review_exports));
8285
+ const pending = countPendingReviews2(coordinatorSession);
8286
+ if (pending instanceof Promise) {
8287
+ pending.then((count) => {
8288
+ if (count > 0) {
8289
+ execSync7(
8290
+ `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
8291
+ { timeout: 3e3 }
8292
+ );
8293
+ logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}", ${count} reviews pending)`);
8294
+ }
8295
+ }).catch(() => {
8296
+ });
8297
+ return true;
8298
+ }
8299
+ } catch {
8300
+ }
7973
8301
  execSync7(
7974
8302
  `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
7975
8303
  { timeout: 3e3 }
@@ -8053,23 +8381,23 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
8053
8381
  const transport = getTransport();
8054
8382
  const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
8055
8383
  const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
8056
- const logDir = path19.join(os9.homedir(), ".exe-os", "session-logs");
8057
- const logFile = path19.join(logDir, `${instanceLabel}-${Date.now()}.log`);
8058
- if (!existsSync15(logDir)) {
8384
+ const logDir = path20.join(os9.homedir(), ".exe-os", "session-logs");
8385
+ const logFile = path20.join(logDir, `${instanceLabel}-${Date.now()}.log`);
8386
+ if (!existsSync16(logDir)) {
8059
8387
  mkdirSync7(logDir, { recursive: true });
8060
8388
  }
8061
8389
  transport.kill(sessionName);
8062
8390
  let cleanupSuffix = "";
8063
8391
  try {
8064
8392
  const thisFile = fileURLToPath2(import.meta.url);
8065
- const cleanupScript = path19.join(path19.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
8066
- if (existsSync15(cleanupScript)) {
8393
+ const cleanupScript = path20.join(path20.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
8394
+ if (existsSync16(cleanupScript)) {
8067
8395
  cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
8068
8396
  }
8069
8397
  } catch {
8070
8398
  }
8071
8399
  try {
8072
- const claudeJsonPath = path19.join(os9.homedir(), ".claude.json");
8400
+ const claudeJsonPath = path20.join(os9.homedir(), ".claude.json");
8073
8401
  let claudeJson = {};
8074
8402
  try {
8075
8403
  claudeJson = JSON.parse(readFileSync12(claudeJsonPath, "utf8"));
@@ -8084,10 +8412,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
8084
8412
  } catch {
8085
8413
  }
8086
8414
  try {
8087
- const settingsDir = path19.join(os9.homedir(), ".claude", "projects");
8415
+ const settingsDir = path20.join(os9.homedir(), ".claude", "projects");
8088
8416
  const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
8089
- const projSettingsDir = path19.join(settingsDir, normalizedKey);
8090
- const settingsPath = path19.join(projSettingsDir, "settings.json");
8417
+ const projSettingsDir = path20.join(settingsDir, normalizedKey);
8418
+ const settingsPath = path20.join(projSettingsDir, "settings.json");
8091
8419
  let settings = {};
8092
8420
  try {
8093
8421
  settings = JSON.parse(readFileSync12(settingsPath, "utf8"));
@@ -8134,7 +8462,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
8134
8462
  let behaviorsFlag = "";
8135
8463
  let legacyFallbackWarned = false;
8136
8464
  if (!useExeAgent && !useBinSymlink) {
8137
- const identityPath2 = path19.join(
8465
+ const identityPath2 = path20.join(
8138
8466
  os9.homedir(),
8139
8467
  ".exe-os",
8140
8468
  "identity",
@@ -8144,13 +8472,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
8144
8472
  const hasAgentFlag = claudeSupportsAgentFlag();
8145
8473
  if (hasAgentFlag) {
8146
8474
  identityFlag = ` --agent ${employeeName}`;
8147
- } else if (existsSync15(identityPath2)) {
8475
+ } else if (existsSync16(identityPath2)) {
8148
8476
  identityFlag = ` --append-system-prompt-file ${identityPath2}`;
8149
8477
  legacyFallbackWarned = true;
8150
8478
  }
8151
8479
  const behaviorsFile = exportBehaviorsSync(
8152
8480
  employeeName,
8153
- path19.basename(spawnCwd),
8481
+ path20.basename(spawnCwd),
8154
8482
  sessionName
8155
8483
  );
8156
8484
  if (behaviorsFile) {
@@ -8165,9 +8493,9 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
8165
8493
  }
8166
8494
  let sessionContextFlag = "";
8167
8495
  try {
8168
- const ctxDir = path19.join(os9.homedir(), ".exe-os", "session-cache");
8496
+ const ctxDir = path20.join(os9.homedir(), ".exe-os", "session-cache");
8169
8497
  mkdirSync7(ctxDir, { recursive: true });
8170
- const ctxFile = path19.join(ctxDir, `session-context-${sessionName}.md`);
8498
+ const ctxFile = path20.join(ctxDir, `session-context-${sessionName}.md`);
8171
8499
  const ctxContent = [
8172
8500
  `## Session Context`,
8173
8501
  `You are running in tmux session: ${sessionName}.`,
@@ -8251,7 +8579,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
8251
8579
  transport.pipeLog(sessionName, logFile);
8252
8580
  try {
8253
8581
  const mySession = getMySession();
8254
- const dispatchInfo = path19.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
8582
+ const dispatchInfo = path20.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
8255
8583
  writeFileSync10(dispatchInfo, JSON.stringify({
8256
8584
  dispatchedBy: mySession,
8257
8585
  rootExe: exeSession,
@@ -8326,15 +8654,15 @@ var init_tmux_routing = __esm({
8326
8654
  init_intercom_queue();
8327
8655
  init_plan_limits();
8328
8656
  init_employees();
8329
- SPAWN_LOCK_DIR = path19.join(os9.homedir(), ".exe-os", "spawn-locks");
8330
- SESSION_CACHE = path19.join(os9.homedir(), ".exe-os", "session-cache");
8657
+ SPAWN_LOCK_DIR = path20.join(os9.homedir(), ".exe-os", "spawn-locks");
8658
+ SESSION_CACHE = path20.join(os9.homedir(), ".exe-os", "session-cache");
8331
8659
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
8332
8660
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
8333
8661
  VERIFY_PANE_LINES = 200;
8334
8662
  INTERCOM_DEBOUNCE_MS = 3e4;
8335
8663
  CODEX_DEBOUNCE_MS = 12e4;
8336
- INTERCOM_LOG2 = path19.join(os9.homedir(), ".exe-os", "intercom.log");
8337
- DEBOUNCE_FILE = path19.join(SESSION_CACHE, "intercom-debounce.json");
8664
+ INTERCOM_LOG2 = path20.join(os9.homedir(), ".exe-os", "intercom.log");
8665
+ DEBOUNCE_FILE = path20.join(SESSION_CACHE, "intercom-debounce.json");
8338
8666
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
8339
8667
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…|• Working|• Ran |• Explored|• Called|esc to interrupt/;
8340
8668
  }
@@ -8375,13 +8703,13 @@ var init_task_scope = __esm({
8375
8703
 
8376
8704
  // src/lib/notifications.ts
8377
8705
  import crypto7 from "crypto";
8378
- import path20 from "path";
8706
+ import path21 from "path";
8379
8707
  import os10 from "os";
8380
8708
  import {
8381
8709
  readFileSync as readFileSync13,
8382
- readdirSync as readdirSync5,
8383
- unlinkSync as unlinkSync5,
8384
- existsSync as existsSync16,
8710
+ readdirSync as readdirSync6,
8711
+ unlinkSync as unlinkSync6,
8712
+ existsSync as existsSync17,
8385
8713
  rmdirSync
8386
8714
  } from "fs";
8387
8715
  async function writeNotification(notification) {
@@ -8510,11 +8838,11 @@ __export(tasks_crud_exports, {
8510
8838
  writeCheckpoint: () => writeCheckpoint
8511
8839
  });
8512
8840
  import crypto8 from "crypto";
8513
- import path21 from "path";
8841
+ import path22 from "path";
8514
8842
  import os11 from "os";
8515
8843
  import { execSync as execSync8 } from "child_process";
8516
8844
  import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
8517
- import { existsSync as existsSync17, readFileSync as readFileSync14 } from "fs";
8845
+ import { existsSync as existsSync18, readFileSync as readFileSync14 } from "fs";
8518
8846
  async function writeCheckpoint(input) {
8519
8847
  const client = getClient();
8520
8848
  const row = await resolveTask(client, input.taskId);
@@ -8708,8 +9036,8 @@ ${scopeMismatchWarning}` : scopeMismatchWarning;
8708
9036
  }
8709
9037
  if (input.baseDir) {
8710
9038
  try {
8711
- await mkdir4(path21.join(input.baseDir, "exe", "output"), { recursive: true });
8712
- await mkdir4(path21.join(input.baseDir, "exe", "research"), { recursive: true });
9039
+ await mkdir4(path22.join(input.baseDir, "exe", "output"), { recursive: true });
9040
+ await mkdir4(path22.join(input.baseDir, "exe", "research"), { recursive: true });
8713
9041
  await ensureArchitectureDoc(input.baseDir, input.projectName);
8714
9042
  await ensureGitignoreExe(input.baseDir);
8715
9043
  } catch {
@@ -8745,10 +9073,10 @@ ${scopeMismatchWarning}` : scopeMismatchWarning;
8745
9073
  });
8746
9074
  if (input.baseDir) {
8747
9075
  try {
8748
- const EXE_OS_DIR = path21.join(os11.homedir(), ".exe-os");
8749
- const mdPath = path21.join(EXE_OS_DIR, taskFile);
8750
- const mdDir = path21.dirname(mdPath);
8751
- if (!existsSync17(mdDir)) await mkdir4(mdDir, { recursive: true });
9076
+ const EXE_OS_DIR = path22.join(os11.homedir(), ".exe-os");
9077
+ const mdPath = path22.join(EXE_OS_DIR, taskFile);
9078
+ const mdDir = path22.dirname(mdPath);
9079
+ if (!existsSync18(mdDir)) await mkdir4(mdDir, { recursive: true });
8752
9080
  const reviewer = input.reviewer ?? input.assignedBy;
8753
9081
  const mdContent = `# ${input.title}
8754
9082
 
@@ -9048,9 +9376,9 @@ async function deleteTaskCore(taskId, _baseDir) {
9048
9376
  return { taskFile, assignedTo, assignedBy, taskSlug };
9049
9377
  }
9050
9378
  async function ensureArchitectureDoc(baseDir, projectName) {
9051
- const archPath = path21.join(baseDir, "exe", "ARCHITECTURE.md");
9379
+ const archPath = path22.join(baseDir, "exe", "ARCHITECTURE.md");
9052
9380
  try {
9053
- if (existsSync17(archPath)) return;
9381
+ if (existsSync18(archPath)) return;
9054
9382
  const template = [
9055
9383
  `# ${projectName} \u2014 System Architecture`,
9056
9384
  "",
@@ -9083,9 +9411,9 @@ async function ensureArchitectureDoc(baseDir, projectName) {
9083
9411
  }
9084
9412
  }
9085
9413
  async function ensureGitignoreExe(baseDir) {
9086
- const gitignorePath = path21.join(baseDir, ".gitignore");
9414
+ const gitignorePath = path22.join(baseDir, ".gitignore");
9087
9415
  try {
9088
- if (existsSync17(gitignorePath)) {
9416
+ if (existsSync18(gitignorePath)) {
9089
9417
  const content = readFileSync14(gitignorePath, "utf-8");
9090
9418
  if (/^\/?exe\/?$/m.test(content)) return;
9091
9419
  await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
@@ -9116,198 +9444,6 @@ var init_tasks_crud = __esm({
9116
9444
  }
9117
9445
  });
9118
9446
 
9119
- // src/lib/tasks-review.ts
9120
- import path22 from "path";
9121
- import { existsSync as existsSync18, readdirSync as readdirSync6, unlinkSync as unlinkSync6 } from "fs";
9122
- async function countPendingReviews(sessionScope) {
9123
- const client = getClient();
9124
- const scope = strictSessionScopeFilter(
9125
- sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
9126
- );
9127
- const result = await client.execute({
9128
- sql: `SELECT COUNT(*) as cnt FROM tasks
9129
- WHERE status = 'needs_review'${scope.sql}`,
9130
- args: [...scope.args]
9131
- });
9132
- return Number(result.rows[0]?.cnt) || 0;
9133
- }
9134
- async function countNewPendingReviewsSince(sinceIso, sessionScope) {
9135
- const client = getClient();
9136
- const scope = strictSessionScopeFilter(
9137
- sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
9138
- );
9139
- const result = await client.execute({
9140
- sql: `SELECT COUNT(*) as cnt FROM tasks
9141
- WHERE status = 'needs_review' AND updated_at > ?${scope.sql}`,
9142
- args: [sinceIso, ...scope.args]
9143
- });
9144
- return Number(result.rows[0]?.cnt) || 0;
9145
- }
9146
- async function listPendingReviews(limit, sessionScope) {
9147
- const client = getClient();
9148
- const scope = strictSessionScopeFilter(
9149
- sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
9150
- );
9151
- const result = await client.execute({
9152
- sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
9153
- WHERE status = 'needs_review'${scope.sql}
9154
- ORDER BY updated_at ASC LIMIT ?`,
9155
- args: [...scope.args, limit]
9156
- });
9157
- return result.rows;
9158
- }
9159
- async function cleanupOrphanedReviews() {
9160
- const client = getClient();
9161
- const now = (/* @__PURE__ */ new Date()).toISOString();
9162
- const r1 = await client.execute({
9163
- sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
9164
- WHERE status IN ('open', 'needs_review', 'in_progress')
9165
- AND assigned_by = 'system'
9166
- AND title LIKE 'Review:%'
9167
- AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))`,
9168
- args: [now]
9169
- });
9170
- const r1b = await client.execute({
9171
- sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
9172
- WHERE status IN ('open', 'needs_review')
9173
- AND title LIKE 'Review:%completed%'
9174
- AND (parent_task_id IS NULL OR parent_task_id NOT IN (SELECT id FROM tasks WHERE status IN ('open', 'in_progress', 'needs_review', 'blocked')))`,
9175
- args: [now]
9176
- });
9177
- const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
9178
- const r2 = await client.execute({
9179
- sql: `UPDATE tasks SET status = 'done', updated_at = ?
9180
- WHERE status = 'needs_review'
9181
- AND result IS NOT NULL
9182
- AND updated_at < ?`,
9183
- args: [now, staleThreshold]
9184
- });
9185
- const total = r1.rowsAffected + (r1b?.rowsAffected ?? 0) + r2.rowsAffected;
9186
- if (total > 0) {
9187
- process.stderr.write(
9188
- `[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r1b?.rowsAffected ?? 0} orphan + ${r2.rowsAffected} stale
9189
- `
9190
- );
9191
- }
9192
- return total;
9193
- }
9194
- function getReviewChecklist(role, agent, taskSlug) {
9195
- const roleLower = role.toLowerCase();
9196
- if (roleLower.includes("engineer") || roleLower === "principal engineer") {
9197
- return {
9198
- lens: "Code Quality (Engineer)",
9199
- checklist: [
9200
- "1. Do all tests pass? Any new tests needed?",
9201
- "2. Is the code clean \u2014 no dead code, no TODOs left?",
9202
- "3. Does it follow existing patterns and conventions in the codebase?",
9203
- "4. Any regressions in the test suite?"
9204
- ]
9205
- };
9206
- }
9207
- if (roleLower === "cto" || roleLower.includes("architect")) {
9208
- return {
9209
- lens: "Architecture (CTO)",
9210
- checklist: [
9211
- "1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
9212
- "2. Is it backward compatible? Any breaking changes?",
9213
- "3. Does it introduce technical debt? Is that debt justified?",
9214
- "4. Security implications? Any new attack surface?",
9215
- "5. Does it scale? Performance considerations?",
9216
- "6. Coordination: does this affect other employees' work or other projects?"
9217
- ]
9218
- };
9219
- }
9220
- if (roleLower === "coo" || roleLower.includes("operations")) {
9221
- return {
9222
- lens: "Strategic (COO)",
9223
- checklist: [
9224
- "1. Does this serve the project mission?",
9225
- "2. Is this the right work at the right time?",
9226
- "3. Does the architectural assessment make sense for the business?",
9227
- "4. Any cross-project implications?"
9228
- ]
9229
- };
9230
- }
9231
- return {
9232
- lens: "General",
9233
- checklist: [
9234
- "1. Read the original task's acceptance criteria",
9235
- `2. Check git log for related commits: \`git log --oneline --author-date-order -10\``,
9236
- "3. Verify code changes match requirements",
9237
- "4. Check if tests were added/updated",
9238
- `5. Look for output files in exe/output/${agent}-${taskSlug}*`
9239
- ]
9240
- };
9241
- }
9242
- async function cleanupReviewFile(row, taskFile, _baseDir) {
9243
- if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
9244
- try {
9245
- const client = getClient();
9246
- const now = (/* @__PURE__ */ new Date()).toISOString();
9247
- const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
9248
- if (parentId) {
9249
- const result = await client.execute({
9250
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
9251
- args: [now, parentId]
9252
- });
9253
- if (result.rowsAffected > 0) {
9254
- process.stderr.write(
9255
- `[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
9256
- `
9257
- );
9258
- }
9259
- } else {
9260
- const fileName = taskFile.split("/").pop() ?? "";
9261
- const reviewPrefix = fileName.replace(".md", "");
9262
- const parts = reviewPrefix.split("-");
9263
- if (parts.length >= 3 && parts[0] === "review") {
9264
- const agent = parts[1];
9265
- const slug = parts.slice(2).join("-");
9266
- const legacyTaskFile = `exe/${agent}/${slug}.md`;
9267
- const result = await client.execute({
9268
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
9269
- args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
9270
- });
9271
- if (result.rowsAffected > 0) {
9272
- process.stderr.write(
9273
- `[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
9274
- `
9275
- );
9276
- }
9277
- }
9278
- }
9279
- } catch (err) {
9280
- process.stderr.write(
9281
- `[review-cleanup] Failed to cascade original task: ${err instanceof Error ? err.message : String(err)}
9282
- `
9283
- );
9284
- }
9285
- try {
9286
- const cacheDir = path22.join(EXE_AI_DIR, "session-cache");
9287
- if (existsSync18(cacheDir)) {
9288
- for (const f of readdirSync6(cacheDir)) {
9289
- if (f.startsWith("review-notified-")) {
9290
- unlinkSync6(path22.join(cacheDir, f));
9291
- }
9292
- }
9293
- }
9294
- } catch {
9295
- }
9296
- }
9297
- var init_tasks_review = __esm({
9298
- "src/lib/tasks-review.ts"() {
9299
- "use strict";
9300
- init_database();
9301
- init_config();
9302
- init_employees();
9303
- init_notifications();
9304
- init_tmux_routing();
9305
- init_session_key();
9306
- init_state_bus();
9307
- init_task_scope();
9308
- }
9309
- });
9310
-
9311
9447
  // src/lib/tasks-chain.ts
9312
9448
  import path23 from "path";
9313
9449
  import { readFile as readFile4, writeFile as writeFile5 } from "fs/promises";
@@ -19823,6 +19959,12 @@ async function cloudSync(config2) {
19823
19959
  pulled = pullResult.records.length;
19824
19960
  }
19825
19961
  }
19962
+ if (pulled > 0) {
19963
+ try {
19964
+ await pushToPostgres(pullResult.records);
19965
+ } catch {
19966
+ }
19967
+ }
19826
19968
  if (pullResult.maxVersion > lastPullVersion) {
19827
19969
  await client.execute({
19828
19970
  sql: "INSERT OR REPLACE INTO sync_meta (key, value) VALUES ('last_cloud_pull_version', ?)",