@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.
@@ -3687,6 +3687,314 @@ var init_capacity_monitor = __esm({
3687
3687
  }
3688
3688
  });
3689
3689
 
3690
+ // src/lib/tasks-review.ts
3691
+ var tasks_review_exports = {};
3692
+ __export(tasks_review_exports, {
3693
+ cleanupOrphanedReviews: () => cleanupOrphanedReviews,
3694
+ cleanupReviewFile: () => cleanupReviewFile,
3695
+ countNewPendingReviewsSince: () => countNewPendingReviewsSince,
3696
+ countPendingReviews: () => countPendingReviews,
3697
+ createReviewForCompletedTask: () => createReviewForCompletedTask,
3698
+ formatAge: () => formatAge,
3699
+ getReviewChecklist: () => getReviewChecklist,
3700
+ isStale: () => isStale,
3701
+ listPendingReviews: () => listPendingReviews
3702
+ });
3703
+ import path11 from "path";
3704
+ import { existsSync as existsSync11, readdirSync as readdirSync2, unlinkSync as unlinkSync2 } from "fs";
3705
+ function formatAge(isoTimestamp) {
3706
+ if (!isoTimestamp) return "";
3707
+ const ms = Date.now() - new Date(isoTimestamp).getTime();
3708
+ if (ms < 0) return "just now";
3709
+ const minutes = Math.floor(ms / 6e4);
3710
+ if (minutes < 60) return `${minutes}m ago`;
3711
+ const hours = Math.floor(minutes / 60);
3712
+ if (hours < 24) return `${hours}h ago`;
3713
+ const days = Math.floor(hours / 24);
3714
+ return `${days}d ago`;
3715
+ }
3716
+ function isStale(isoTimestamp) {
3717
+ if (!isoTimestamp) return false;
3718
+ return Date.now() - new Date(isoTimestamp).getTime() > 24 * 60 * 60 * 1e3;
3719
+ }
3720
+ async function countPendingReviews(sessionScope) {
3721
+ const client = getClient();
3722
+ const scope = strictSessionScopeFilter(
3723
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
3724
+ );
3725
+ const result = await client.execute({
3726
+ sql: `SELECT COUNT(*) as cnt FROM tasks
3727
+ WHERE status = 'needs_review'${scope.sql}`,
3728
+ args: [...scope.args]
3729
+ });
3730
+ return Number(result.rows[0]?.cnt) || 0;
3731
+ }
3732
+ async function countNewPendingReviewsSince(sinceIso, sessionScope) {
3733
+ const client = getClient();
3734
+ const scope = strictSessionScopeFilter(
3735
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
3736
+ );
3737
+ const result = await client.execute({
3738
+ sql: `SELECT COUNT(*) as cnt FROM tasks
3739
+ WHERE status = 'needs_review' AND updated_at > ?${scope.sql}`,
3740
+ args: [sinceIso, ...scope.args]
3741
+ });
3742
+ return Number(result.rows[0]?.cnt) || 0;
3743
+ }
3744
+ async function listPendingReviews(limit, sessionScope) {
3745
+ const client = getClient();
3746
+ const scope = strictSessionScopeFilter(
3747
+ sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
3748
+ );
3749
+ const result = await client.execute({
3750
+ sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
3751
+ WHERE status = 'needs_review'${scope.sql}
3752
+ ORDER BY updated_at ASC LIMIT ?`,
3753
+ args: [...scope.args, limit]
3754
+ });
3755
+ return result.rows;
3756
+ }
3757
+ async function cleanupOrphanedReviews() {
3758
+ const client = getClient();
3759
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3760
+ const r1 = await client.execute({
3761
+ sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
3762
+ WHERE status IN ('open', 'needs_review', 'in_progress')
3763
+ AND assigned_by = 'system'
3764
+ AND title LIKE 'Review:%'
3765
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))`,
3766
+ args: [now]
3767
+ });
3768
+ const r1b = await client.execute({
3769
+ sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
3770
+ WHERE status IN ('open', 'needs_review')
3771
+ AND title LIKE 'Review:%completed%'
3772
+ 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')))`,
3773
+ args: [now]
3774
+ });
3775
+ const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
3776
+ const r2 = await client.execute({
3777
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
3778
+ WHERE status = 'needs_review'
3779
+ AND result IS NOT NULL
3780
+ AND updated_at < ?`,
3781
+ args: [now, staleThreshold]
3782
+ });
3783
+ const total = r1.rowsAffected + (r1b?.rowsAffected ?? 0) + r2.rowsAffected;
3784
+ if (total > 0) {
3785
+ process.stderr.write(
3786
+ `[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r1b?.rowsAffected ?? 0} orphan + ${r2.rowsAffected} stale
3787
+ `
3788
+ );
3789
+ }
3790
+ return total;
3791
+ }
3792
+ function getReviewChecklist(role, agent, taskSlug) {
3793
+ const roleLower = role.toLowerCase();
3794
+ if (roleLower.includes("engineer") || roleLower === "principal engineer") {
3795
+ return {
3796
+ lens: "Code Quality (Engineer)",
3797
+ checklist: [
3798
+ "1. Do all tests pass? Any new tests needed?",
3799
+ "2. Is the code clean \u2014 no dead code, no TODOs left?",
3800
+ "3. Does it follow existing patterns and conventions in the codebase?",
3801
+ "4. Any regressions in the test suite?"
3802
+ ]
3803
+ };
3804
+ }
3805
+ if (roleLower === "cto" || roleLower.includes("architect")) {
3806
+ return {
3807
+ lens: "Architecture (CTO)",
3808
+ checklist: [
3809
+ "1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
3810
+ "2. Is it backward compatible? Any breaking changes?",
3811
+ "3. Does it introduce technical debt? Is that debt justified?",
3812
+ "4. Security implications? Any new attack surface?",
3813
+ "5. Does it scale? Performance considerations?",
3814
+ "6. Coordination: does this affect other employees' work or other projects?"
3815
+ ]
3816
+ };
3817
+ }
3818
+ if (roleLower === "coo" || roleLower.includes("operations")) {
3819
+ return {
3820
+ lens: "Strategic (COO)",
3821
+ checklist: [
3822
+ "1. Does this serve the project mission?",
3823
+ "2. Is this the right work at the right time?",
3824
+ "3. Does the architectural assessment make sense for the business?",
3825
+ "4. Any cross-project implications?"
3826
+ ]
3827
+ };
3828
+ }
3829
+ return {
3830
+ lens: "General",
3831
+ checklist: [
3832
+ "1. Read the original task's acceptance criteria",
3833
+ `2. Check git log for related commits: \`git log --oneline --author-date-order -10\``,
3834
+ "3. Verify code changes match requirements",
3835
+ "4. Check if tests were added/updated",
3836
+ `5. Look for output files in exe/output/${agent}-${taskSlug}*`
3837
+ ]
3838
+ };
3839
+ }
3840
+ async function createReviewForCompletedTask(row, result, _baseDir, now) {
3841
+ const taskFile = String(row.task_file);
3842
+ const employees = await loadEmployees();
3843
+ const coordinatorName = getCoordinatorName(employees);
3844
+ if (isCoordinatorName(String(row.assigned_to), employees)) return;
3845
+ if (String(row.title).startsWith("Review:")) return;
3846
+ const fileName = taskFile.split("/").pop() ?? "";
3847
+ if (fileName.startsWith("review-") && String(row.assigned_by) === "system") return;
3848
+ if (fileName.startsWith("review-") && String(row.assigned_to) === "system") return;
3849
+ const client = getClient();
3850
+ const agent = String(row.assigned_to);
3851
+ const rawReviewer = row.reviewer || row.assigned_by;
3852
+ const reviewer = rawReviewer ? String(rawReviewer) : coordinatorName;
3853
+ const currentStatus = String(row.status ?? "");
3854
+ if (currentStatus === "done") return;
3855
+ const existingResult = String(row.result ?? "");
3856
+ if (existingResult.includes("## Review notes")) return;
3857
+ let reviewerRole = "unknown";
3858
+ try {
3859
+ const emp = getEmployee(employees, reviewer);
3860
+ if (emp) reviewerRole = emp.role;
3861
+ } catch {
3862
+ }
3863
+ const taskTitle = String(row.title);
3864
+ const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "unknown";
3865
+ const { lens, checklist } = getReviewChecklist(reviewerRole, agent, taskSlug);
3866
+ process.stderr.write(
3867
+ `[review] Annotating "${taskTitle}" for review by ${reviewer} (${reviewerRole})
3868
+ `
3869
+ );
3870
+ const reviewNotes = [
3871
+ `
3872
+ ---
3873
+ ## Review notes`,
3874
+ `Review lens: ${lens}`,
3875
+ `Reviewer: **${reviewer}** (${reviewerRole})`,
3876
+ "",
3877
+ "### Checklist",
3878
+ ...checklist,
3879
+ "",
3880
+ "### Verdict",
3881
+ "- **Approved:** mark this task as done",
3882
+ "- **Needs work:** re-open with notes"
3883
+ ].join("\n");
3884
+ const originalTaskId = String(row.id);
3885
+ const updatedResult = (result ?? "No result summary provided") + reviewNotes;
3886
+ await client.execute({
3887
+ sql: `UPDATE tasks SET result = ?, status = 'needs_review', updated_at = ?
3888
+ WHERE id = ?`,
3889
+ args: [updatedResult, now, originalTaskId]
3890
+ });
3891
+ orgBus.emit({
3892
+ type: "review_created",
3893
+ reviewId: originalTaskId,
3894
+ employee: agent,
3895
+ reviewer,
3896
+ timestamp: now
3897
+ });
3898
+ await writeNotification({
3899
+ agentId: agent,
3900
+ agentRole: String(row.assigned_to),
3901
+ event: "task_complete",
3902
+ project: String(row.project_name),
3903
+ summary: `completed "${taskTitle}" \u2014 ready for review`,
3904
+ taskFile
3905
+ });
3906
+ const originalPriority = String(row.priority).toLowerCase();
3907
+ const autoApprove = originalPriority === "p2" && result?.toLowerCase().includes("tests pass");
3908
+ if (!autoApprove) {
3909
+ try {
3910
+ const key = getSessionKey();
3911
+ const exeSession = getParentExe(key);
3912
+ if (exeSession) {
3913
+ sendIntercom(exeSession);
3914
+ }
3915
+ } catch {
3916
+ }
3917
+ }
3918
+ if (autoApprove) {
3919
+ process.stderr.write(
3920
+ `[review] Auto-approving "${taskTitle}" (P2 + tests pass)
3921
+ `
3922
+ );
3923
+ await client.execute({
3924
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ?",
3925
+ args: [now, originalTaskId]
3926
+ });
3927
+ }
3928
+ }
3929
+ async function cleanupReviewFile(row, taskFile, _baseDir) {
3930
+ if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
3931
+ try {
3932
+ const client = getClient();
3933
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3934
+ const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
3935
+ if (parentId) {
3936
+ const result = await client.execute({
3937
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
3938
+ args: [now, parentId]
3939
+ });
3940
+ if (result.rowsAffected > 0) {
3941
+ process.stderr.write(
3942
+ `[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
3943
+ `
3944
+ );
3945
+ }
3946
+ } else {
3947
+ const fileName = taskFile.split("/").pop() ?? "";
3948
+ const reviewPrefix = fileName.replace(".md", "");
3949
+ const parts = reviewPrefix.split("-");
3950
+ if (parts.length >= 3 && parts[0] === "review") {
3951
+ const agent = parts[1];
3952
+ const slug = parts.slice(2).join("-");
3953
+ const legacyTaskFile = `exe/${agent}/${slug}.md`;
3954
+ const result = await client.execute({
3955
+ sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
3956
+ args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
3957
+ });
3958
+ if (result.rowsAffected > 0) {
3959
+ process.stderr.write(
3960
+ `[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
3961
+ `
3962
+ );
3963
+ }
3964
+ }
3965
+ }
3966
+ } catch (err) {
3967
+ process.stderr.write(
3968
+ `[review-cleanup] Failed to cascade original task: ${err instanceof Error ? err.message : String(err)}
3969
+ `
3970
+ );
3971
+ }
3972
+ try {
3973
+ const cacheDir = path11.join(EXE_AI_DIR, "session-cache");
3974
+ if (existsSync11(cacheDir)) {
3975
+ for (const f of readdirSync2(cacheDir)) {
3976
+ if (f.startsWith("review-notified-")) {
3977
+ unlinkSync2(path11.join(cacheDir, f));
3978
+ }
3979
+ }
3980
+ }
3981
+ } catch {
3982
+ }
3983
+ }
3984
+ var init_tasks_review = __esm({
3985
+ "src/lib/tasks-review.ts"() {
3986
+ "use strict";
3987
+ init_database();
3988
+ init_config();
3989
+ init_employees();
3990
+ init_notifications();
3991
+ init_tmux_routing();
3992
+ init_session_key();
3993
+ init_state_bus();
3994
+ init_task_scope();
3995
+ }
3996
+ });
3997
+
3690
3998
  // src/lib/tmux-routing.ts
3691
3999
  var tmux_routing_exports = {};
3692
4000
  __export(tmux_routing_exports, {
@@ -3713,13 +4021,13 @@ __export(tmux_routing_exports, {
3713
4021
  verifyPaneAtCapacity: () => verifyPaneAtCapacity
3714
4022
  });
3715
4023
  import { execFileSync as execFileSync2, execSync as execSync4 } from "child_process";
3716
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6, existsSync as existsSync11, appendFileSync, readdirSync as readdirSync2 } from "fs";
3717
- import path11 from "path";
4024
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6, existsSync as existsSync12, appendFileSync, readdirSync as readdirSync3 } from "fs";
4025
+ import path12 from "path";
3718
4026
  import os8 from "os";
3719
4027
  import { fileURLToPath } from "url";
3720
- import { unlinkSync as unlinkSync2 } from "fs";
4028
+ import { unlinkSync as unlinkSync3 } from "fs";
3721
4029
  function spawnLockPath(sessionName) {
3722
- return path11.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
4030
+ return path12.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
3723
4031
  }
3724
4032
  function isProcessAlive(pid) {
3725
4033
  try {
@@ -3730,11 +4038,11 @@ function isProcessAlive(pid) {
3730
4038
  }
3731
4039
  }
3732
4040
  function acquireSpawnLock(sessionName) {
3733
- if (!existsSync11(SPAWN_LOCK_DIR)) {
4041
+ if (!existsSync12(SPAWN_LOCK_DIR)) {
3734
4042
  mkdirSync6(SPAWN_LOCK_DIR, { recursive: true });
3735
4043
  }
3736
4044
  const lockFile = spawnLockPath(sessionName);
3737
- if (existsSync11(lockFile)) {
4045
+ if (existsSync12(lockFile)) {
3738
4046
  try {
3739
4047
  const lock = JSON.parse(readFileSync8(lockFile, "utf8"));
3740
4048
  const age = Date.now() - lock.timestamp;
@@ -3749,20 +4057,20 @@ function acquireSpawnLock(sessionName) {
3749
4057
  }
3750
4058
  function releaseSpawnLock(sessionName) {
3751
4059
  try {
3752
- unlinkSync2(spawnLockPath(sessionName));
4060
+ unlinkSync3(spawnLockPath(sessionName));
3753
4061
  } catch {
3754
4062
  }
3755
4063
  }
3756
4064
  function resolveBehaviorsExporterScript() {
3757
4065
  try {
3758
4066
  const thisFile = fileURLToPath(import.meta.url);
3759
- const scriptPath = path11.join(
3760
- path11.dirname(thisFile),
4067
+ const scriptPath = path12.join(
4068
+ path12.dirname(thisFile),
3761
4069
  "..",
3762
4070
  "bin",
3763
4071
  "exe-export-behaviors.js"
3764
4072
  );
3765
- return existsSync11(scriptPath) ? scriptPath : null;
4073
+ return existsSync12(scriptPath) ? scriptPath : null;
3766
4074
  } catch {
3767
4075
  return null;
3768
4076
  }
@@ -3828,11 +4136,11 @@ function extractRootExe(name) {
3828
4136
  return parts.length > 0 ? parts[parts.length - 1] : null;
3829
4137
  }
3830
4138
  function registerParentExe(sessionKey, parentExe, dispatchedBy) {
3831
- if (!existsSync11(SESSION_CACHE)) {
4139
+ if (!existsSync12(SESSION_CACHE)) {
3832
4140
  mkdirSync6(SESSION_CACHE, { recursive: true });
3833
4141
  }
3834
4142
  const rootExe = extractRootExe(parentExe) ?? parentExe;
3835
- const filePath = path11.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
4143
+ const filePath = path12.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
3836
4144
  writeFileSync6(filePath, JSON.stringify({
3837
4145
  parentExe: rootExe,
3838
4146
  dispatchedBy: dispatchedBy || rootExe,
@@ -3841,7 +4149,7 @@ function registerParentExe(sessionKey, parentExe, dispatchedBy) {
3841
4149
  }
3842
4150
  function getParentExe(sessionKey) {
3843
4151
  try {
3844
- const data = JSON.parse(readFileSync8(path11.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
4152
+ const data = JSON.parse(readFileSync8(path12.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
3845
4153
  return data.parentExe || null;
3846
4154
  } catch {
3847
4155
  return null;
@@ -3850,7 +4158,7 @@ function getParentExe(sessionKey) {
3850
4158
  function getDispatchedBy(sessionKey) {
3851
4159
  try {
3852
4160
  const data = JSON.parse(readFileSync8(
3853
- path11.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
4161
+ path12.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
3854
4162
  "utf8"
3855
4163
  ));
3856
4164
  return data.dispatchedBy ?? data.parentExe ?? null;
@@ -3920,7 +4228,7 @@ async function verifyPaneAtCapacity(sessionName) {
3920
4228
  }
3921
4229
  function readDebounceState() {
3922
4230
  try {
3923
- if (!existsSync11(DEBOUNCE_FILE)) return {};
4231
+ if (!existsSync12(DEBOUNCE_FILE)) return {};
3924
4232
  const raw = JSON.parse(readFileSync8(DEBOUNCE_FILE, "utf8"));
3925
4233
  const state = {};
3926
4234
  for (const [key, val] of Object.entries(raw)) {
@@ -3937,7 +4245,7 @@ function readDebounceState() {
3937
4245
  }
3938
4246
  function writeDebounceState(state) {
3939
4247
  try {
3940
- if (!existsSync11(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
4248
+ if (!existsSync12(SESSION_CACHE)) mkdirSync6(SESSION_CACHE, { recursive: true });
3941
4249
  writeFileSync6(DEBOUNCE_FILE, JSON.stringify(state));
3942
4250
  } catch {
3943
4251
  }
@@ -4033,22 +4341,24 @@ function sendIntercom(targetSession) {
4033
4341
  logIntercom(`QUEUED \u2192 ${targetSession} (session busy)${batched2 > 0 ? ` [${batched2} batched]` : ""}`);
4034
4342
  return "queued";
4035
4343
  }
4036
- try {
4037
- const rawAgent = targetSession.split("-")[0] ?? targetSession;
4038
- const agent = baseAgentName(rawAgent);
4039
- const markerPath = path11.join(SESSION_CACHE, `current-task-${agent}.json`);
4040
- if (existsSync11(markerPath)) {
4041
- logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker \u2014 will auto-chain)`);
4042
- return "debounced";
4344
+ if (sessionState !== "idle") {
4345
+ try {
4346
+ const rawAgent = targetSession.split("-")[0] ?? targetSession;
4347
+ const agent = baseAgentName(rawAgent);
4348
+ const markerPath = path12.join(SESSION_CACHE, `current-task-${agent}.json`);
4349
+ if (existsSync12(markerPath)) {
4350
+ logIntercom(`SKIP \u2192 ${targetSession} (has in_progress task marker + not idle \u2014 will auto-chain)`);
4351
+ return "debounced";
4352
+ }
4353
+ } catch {
4043
4354
  }
4044
- } catch {
4045
4355
  }
4046
4356
  try {
4047
4357
  const rawAgent = targetSession.split("-")[0] ?? targetSession;
4048
4358
  const agent = baseAgentName(rawAgent);
4049
- const taskDir = path11.join(process.cwd(), "exe", agent);
4050
- if (existsSync11(taskDir)) {
4051
- const files = readdirSync2(taskDir).filter(
4359
+ const taskDir = path12.join(process.cwd(), "exe", agent);
4360
+ if (existsSync12(taskDir)) {
4361
+ const files = readdirSync3(taskDir).filter(
4052
4362
  (f) => f.endsWith(".md") && f !== "DONE.txt"
4053
4363
  );
4054
4364
  if (files.length === 0) {
@@ -4112,6 +4422,24 @@ function notifyCoordinatorTaskCompletion(coordinatorSession, agentName, taskTitl
4112
4422
  try {
4113
4423
  const sessions = transport.listSessions();
4114
4424
  if (!sessions.includes(coordinatorSession)) return false;
4425
+ try {
4426
+ const { countPendingReviews: countPendingReviews2 } = (init_tasks_review(), __toCommonJS(tasks_review_exports));
4427
+ const pending = countPendingReviews2(coordinatorSession);
4428
+ if (pending instanceof Promise) {
4429
+ pending.then((count) => {
4430
+ if (count > 0) {
4431
+ execSync4(
4432
+ `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
4433
+ { timeout: 3e3 }
4434
+ );
4435
+ logIntercom(`COMPLETION \u2192 ${coordinatorSession} (${agentName} completed "${taskTitle.slice(0, 50)}", ${count} reviews pending)`);
4436
+ }
4437
+ }).catch(() => {
4438
+ });
4439
+ return true;
4440
+ }
4441
+ } catch {
4442
+ }
4115
4443
  execSync4(
4116
4444
  `tmux send-keys -t ${JSON.stringify(coordinatorSession)} '/exe-intercom' Enter`,
4117
4445
  { timeout: 3e3 }
@@ -4195,23 +4523,23 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4195
4523
  const transport = getTransport();
4196
4524
  const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
4197
4525
  const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
4198
- const logDir = path11.join(os8.homedir(), ".exe-os", "session-logs");
4199
- const logFile = path11.join(logDir, `${instanceLabel}-${Date.now()}.log`);
4200
- if (!existsSync11(logDir)) {
4526
+ const logDir = path12.join(os8.homedir(), ".exe-os", "session-logs");
4527
+ const logFile = path12.join(logDir, `${instanceLabel}-${Date.now()}.log`);
4528
+ if (!existsSync12(logDir)) {
4201
4529
  mkdirSync6(logDir, { recursive: true });
4202
4530
  }
4203
4531
  transport.kill(sessionName);
4204
4532
  let cleanupSuffix = "";
4205
4533
  try {
4206
4534
  const thisFile = fileURLToPath(import.meta.url);
4207
- const cleanupScript = path11.join(path11.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
4208
- if (existsSync11(cleanupScript)) {
4535
+ const cleanupScript = path12.join(path12.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
4536
+ if (existsSync12(cleanupScript)) {
4209
4537
  cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
4210
4538
  }
4211
4539
  } catch {
4212
4540
  }
4213
4541
  try {
4214
- const claudeJsonPath = path11.join(os8.homedir(), ".claude.json");
4542
+ const claudeJsonPath = path12.join(os8.homedir(), ".claude.json");
4215
4543
  let claudeJson = {};
4216
4544
  try {
4217
4545
  claudeJson = JSON.parse(readFileSync8(claudeJsonPath, "utf8"));
@@ -4226,10 +4554,10 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4226
4554
  } catch {
4227
4555
  }
4228
4556
  try {
4229
- const settingsDir = path11.join(os8.homedir(), ".claude", "projects");
4557
+ const settingsDir = path12.join(os8.homedir(), ".claude", "projects");
4230
4558
  const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
4231
- const projSettingsDir = path11.join(settingsDir, normalizedKey);
4232
- const settingsPath = path11.join(projSettingsDir, "settings.json");
4559
+ const projSettingsDir = path12.join(settingsDir, normalizedKey);
4560
+ const settingsPath = path12.join(projSettingsDir, "settings.json");
4233
4561
  let settings = {};
4234
4562
  try {
4235
4563
  settings = JSON.parse(readFileSync8(settingsPath, "utf8"));
@@ -4276,7 +4604,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4276
4604
  let behaviorsFlag = "";
4277
4605
  let legacyFallbackWarned = false;
4278
4606
  if (!useExeAgent && !useBinSymlink) {
4279
- const identityPath = path11.join(
4607
+ const identityPath = path12.join(
4280
4608
  os8.homedir(),
4281
4609
  ".exe-os",
4282
4610
  "identity",
@@ -4286,13 +4614,13 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4286
4614
  const hasAgentFlag = claudeSupportsAgentFlag();
4287
4615
  if (hasAgentFlag) {
4288
4616
  identityFlag = ` --agent ${employeeName}`;
4289
- } else if (existsSync11(identityPath)) {
4617
+ } else if (existsSync12(identityPath)) {
4290
4618
  identityFlag = ` --append-system-prompt-file ${identityPath}`;
4291
4619
  legacyFallbackWarned = true;
4292
4620
  }
4293
4621
  const behaviorsFile = exportBehaviorsSync(
4294
4622
  employeeName,
4295
- path11.basename(spawnCwd),
4623
+ path12.basename(spawnCwd),
4296
4624
  sessionName
4297
4625
  );
4298
4626
  if (behaviorsFile) {
@@ -4307,9 +4635,9 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4307
4635
  }
4308
4636
  let sessionContextFlag = "";
4309
4637
  try {
4310
- const ctxDir = path11.join(os8.homedir(), ".exe-os", "session-cache");
4638
+ const ctxDir = path12.join(os8.homedir(), ".exe-os", "session-cache");
4311
4639
  mkdirSync6(ctxDir, { recursive: true });
4312
- const ctxFile = path11.join(ctxDir, `session-context-${sessionName}.md`);
4640
+ const ctxFile = path12.join(ctxDir, `session-context-${sessionName}.md`);
4313
4641
  const ctxContent = [
4314
4642
  `## Session Context`,
4315
4643
  `You are running in tmux session: ${sessionName}.`,
@@ -4393,7 +4721,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
4393
4721
  transport.pipeLog(sessionName, logFile);
4394
4722
  try {
4395
4723
  const mySession = getMySession();
4396
- const dispatchInfo = path11.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
4724
+ const dispatchInfo = path12.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
4397
4725
  writeFileSync6(dispatchInfo, JSON.stringify({
4398
4726
  dispatchedBy: mySession,
4399
4727
  rootExe: exeSession,
@@ -4468,15 +4796,15 @@ var init_tmux_routing = __esm({
4468
4796
  init_intercom_queue();
4469
4797
  init_plan_limits();
4470
4798
  init_employees();
4471
- SPAWN_LOCK_DIR = path11.join(os8.homedir(), ".exe-os", "spawn-locks");
4472
- SESSION_CACHE = path11.join(os8.homedir(), ".exe-os", "session-cache");
4799
+ SPAWN_LOCK_DIR = path12.join(os8.homedir(), ".exe-os", "spawn-locks");
4800
+ SESSION_CACHE = path12.join(os8.homedir(), ".exe-os", "session-cache");
4473
4801
  BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
4474
4802
  VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
4475
4803
  VERIFY_PANE_LINES = 200;
4476
4804
  INTERCOM_DEBOUNCE_MS = 3e4;
4477
4805
  CODEX_DEBOUNCE_MS = 12e4;
4478
- INTERCOM_LOG2 = path11.join(os8.homedir(), ".exe-os", "intercom.log");
4479
- DEBOUNCE_FILE = path11.join(SESSION_CACHE, "intercom-debounce.json");
4806
+ INTERCOM_LOG2 = path12.join(os8.homedir(), ".exe-os", "intercom.log");
4807
+ DEBOUNCE_FILE = path12.join(SESSION_CACHE, "intercom-debounce.json");
4480
4808
  DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
4481
4809
  BUSY_PATTERN = /[✻✽✶✳·].*…|Running…|• Working|• Ran |• Explored|• Called|esc to interrupt/;
4482
4810
  }
@@ -4517,13 +4845,13 @@ var init_task_scope = __esm({
4517
4845
 
4518
4846
  // src/lib/notifications.ts
4519
4847
  import crypto2 from "crypto";
4520
- import path12 from "path";
4848
+ import path13 from "path";
4521
4849
  import os9 from "os";
4522
4850
  import {
4523
4851
  readFileSync as readFileSync9,
4524
- readdirSync as readdirSync3,
4525
- unlinkSync as unlinkSync3,
4526
- existsSync as existsSync12,
4852
+ readdirSync as readdirSync4,
4853
+ unlinkSync as unlinkSync4,
4854
+ existsSync as existsSync13,
4527
4855
  rmdirSync
4528
4856
  } from "fs";
4529
4857
  async function writeNotification(notification) {
@@ -4574,7 +4902,7 @@ var init_notifications = __esm({
4574
4902
 
4575
4903
  // src/lib/project-name.ts
4576
4904
  import { execSync as execSync5 } from "child_process";
4577
- import path13 from "path";
4905
+ import path14 from "path";
4578
4906
  function getProjectName(cwd) {
4579
4907
  const dir = cwd ?? process.cwd();
4580
4908
  if (_cached2 && _cachedCwd === dir) return _cached2;
@@ -4587,7 +4915,7 @@ function getProjectName(cwd) {
4587
4915
  timeout: 2e3,
4588
4916
  stdio: ["pipe", "pipe", "pipe"]
4589
4917
  }).trim();
4590
- repoRoot = path13.dirname(gitCommonDir);
4918
+ repoRoot = path14.dirname(gitCommonDir);
4591
4919
  } catch {
4592
4920
  repoRoot = execSync5("git rev-parse --show-toplevel", {
4593
4921
  cwd: dir,
@@ -4596,11 +4924,11 @@ function getProjectName(cwd) {
4596
4924
  stdio: ["pipe", "pipe", "pipe"]
4597
4925
  }).trim();
4598
4926
  }
4599
- _cached2 = path13.basename(repoRoot);
4927
+ _cached2 = path14.basename(repoRoot);
4600
4928
  _cachedCwd = dir;
4601
4929
  return _cached2;
4602
4930
  } catch {
4603
- _cached2 = path13.basename(dir);
4931
+ _cached2 = path14.basename(dir);
4604
4932
  _cachedCwd = dir;
4605
4933
  return _cached2;
4606
4934
  }
@@ -4678,11 +5006,11 @@ var init_session_scope = __esm({
4678
5006
 
4679
5007
  // src/lib/tasks-crud.ts
4680
5008
  import crypto3 from "crypto";
4681
- import path14 from "path";
5009
+ import path15 from "path";
4682
5010
  import os10 from "os";
4683
5011
  import { execSync as execSync6 } from "child_process";
4684
5012
  import { mkdir as mkdir4, writeFile as writeFile4, appendFile } from "fs/promises";
4685
- import { existsSync as existsSync13, readFileSync as readFileSync10 } from "fs";
5013
+ import { existsSync as existsSync14, readFileSync as readFileSync10 } from "fs";
4686
5014
  async function writeCheckpoint(input) {
4687
5015
  const client = getClient();
4688
5016
  const row = await resolveTask(client, input.taskId);
@@ -4876,8 +5204,8 @@ ${scopeMismatchWarning}` : scopeMismatchWarning;
4876
5204
  }
4877
5205
  if (input.baseDir) {
4878
5206
  try {
4879
- await mkdir4(path14.join(input.baseDir, "exe", "output"), { recursive: true });
4880
- await mkdir4(path14.join(input.baseDir, "exe", "research"), { recursive: true });
5207
+ await mkdir4(path15.join(input.baseDir, "exe", "output"), { recursive: true });
5208
+ await mkdir4(path15.join(input.baseDir, "exe", "research"), { recursive: true });
4881
5209
  await ensureArchitectureDoc(input.baseDir, input.projectName);
4882
5210
  await ensureGitignoreExe(input.baseDir);
4883
5211
  } catch {
@@ -4913,10 +5241,10 @@ ${scopeMismatchWarning}` : scopeMismatchWarning;
4913
5241
  });
4914
5242
  if (input.baseDir) {
4915
5243
  try {
4916
- const EXE_OS_DIR = path14.join(os10.homedir(), ".exe-os");
4917
- const mdPath = path14.join(EXE_OS_DIR, taskFile);
4918
- const mdDir = path14.dirname(mdPath);
4919
- if (!existsSync13(mdDir)) await mkdir4(mdDir, { recursive: true });
5244
+ const EXE_OS_DIR = path15.join(os10.homedir(), ".exe-os");
5245
+ const mdPath = path15.join(EXE_OS_DIR, taskFile);
5246
+ const mdDir = path15.dirname(mdPath);
5247
+ if (!existsSync14(mdDir)) await mkdir4(mdDir, { recursive: true });
4920
5248
  const reviewer = input.reviewer ?? input.assignedBy;
4921
5249
  const mdContent = `# ${input.title}
4922
5250
 
@@ -5216,9 +5544,9 @@ async function deleteTaskCore(taskId, _baseDir) {
5216
5544
  return { taskFile, assignedTo, assignedBy, taskSlug };
5217
5545
  }
5218
5546
  async function ensureArchitectureDoc(baseDir, projectName) {
5219
- const archPath = path14.join(baseDir, "exe", "ARCHITECTURE.md");
5547
+ const archPath = path15.join(baseDir, "exe", "ARCHITECTURE.md");
5220
5548
  try {
5221
- if (existsSync13(archPath)) return;
5549
+ if (existsSync14(archPath)) return;
5222
5550
  const template = [
5223
5551
  `# ${projectName} \u2014 System Architecture`,
5224
5552
  "",
@@ -5251,9 +5579,9 @@ async function ensureArchitectureDoc(baseDir, projectName) {
5251
5579
  }
5252
5580
  }
5253
5581
  async function ensureGitignoreExe(baseDir) {
5254
- const gitignorePath = path14.join(baseDir, ".gitignore");
5582
+ const gitignorePath = path15.join(baseDir, ".gitignore");
5255
5583
  try {
5256
- if (existsSync13(gitignorePath)) {
5584
+ if (existsSync14(gitignorePath)) {
5257
5585
  const content = readFileSync10(gitignorePath, "utf-8");
5258
5586
  if (/^\/?exe\/?$/m.test(content)) return;
5259
5587
  await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
@@ -5284,198 +5612,6 @@ var init_tasks_crud = __esm({
5284
5612
  }
5285
5613
  });
5286
5614
 
5287
- // src/lib/tasks-review.ts
5288
- import path15 from "path";
5289
- import { existsSync as existsSync14, readdirSync as readdirSync4, unlinkSync as unlinkSync4 } from "fs";
5290
- async function countPendingReviews(sessionScope) {
5291
- const client = getClient();
5292
- const scope = strictSessionScopeFilter(
5293
- sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
5294
- );
5295
- const result = await client.execute({
5296
- sql: `SELECT COUNT(*) as cnt FROM tasks
5297
- WHERE status = 'needs_review'${scope.sql}`,
5298
- args: [...scope.args]
5299
- });
5300
- return Number(result.rows[0]?.cnt) || 0;
5301
- }
5302
- async function countNewPendingReviewsSince(sinceIso, sessionScope) {
5303
- const client = getClient();
5304
- const scope = strictSessionScopeFilter(
5305
- sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
5306
- );
5307
- const result = await client.execute({
5308
- sql: `SELECT COUNT(*) as cnt FROM tasks
5309
- WHERE status = 'needs_review' AND updated_at > ?${scope.sql}`,
5310
- args: [sinceIso, ...scope.args]
5311
- });
5312
- return Number(result.rows[0]?.cnt) || 0;
5313
- }
5314
- async function listPendingReviews(limit, sessionScope) {
5315
- const client = getClient();
5316
- const scope = strictSessionScopeFilter(
5317
- sessionScope === void 0 ? getCurrentSessionScope() : sessionScope
5318
- );
5319
- const result = await client.execute({
5320
- sql: `SELECT title, assigned_to, project_name, updated_at FROM tasks
5321
- WHERE status = 'needs_review'${scope.sql}
5322
- ORDER BY updated_at ASC LIMIT ?`,
5323
- args: [...scope.args, limit]
5324
- });
5325
- return result.rows;
5326
- }
5327
- async function cleanupOrphanedReviews() {
5328
- const client = getClient();
5329
- const now = (/* @__PURE__ */ new Date()).toISOString();
5330
- const r1 = await client.execute({
5331
- sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
5332
- WHERE status IN ('open', 'needs_review', 'in_progress')
5333
- AND assigned_by = 'system'
5334
- AND title LIKE 'Review:%'
5335
- AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled', 'closed'))`,
5336
- args: [now]
5337
- });
5338
- const r1b = await client.execute({
5339
- sql: `UPDATE tasks SET status = 'cancelled', updated_at = ?
5340
- WHERE status IN ('open', 'needs_review')
5341
- AND title LIKE 'Review:%completed%'
5342
- 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')))`,
5343
- args: [now]
5344
- });
5345
- const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
5346
- const r2 = await client.execute({
5347
- sql: `UPDATE tasks SET status = 'done', updated_at = ?
5348
- WHERE status = 'needs_review'
5349
- AND result IS NOT NULL
5350
- AND updated_at < ?`,
5351
- args: [now, staleThreshold]
5352
- });
5353
- const total = r1.rowsAffected + (r1b?.rowsAffected ?? 0) + r2.rowsAffected;
5354
- if (total > 0) {
5355
- process.stderr.write(
5356
- `[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r1b?.rowsAffected ?? 0} orphan + ${r2.rowsAffected} stale
5357
- `
5358
- );
5359
- }
5360
- return total;
5361
- }
5362
- function getReviewChecklist(role, agent, taskSlug) {
5363
- const roleLower = role.toLowerCase();
5364
- if (roleLower.includes("engineer") || roleLower === "principal engineer") {
5365
- return {
5366
- lens: "Code Quality (Engineer)",
5367
- checklist: [
5368
- "1. Do all tests pass? Any new tests needed?",
5369
- "2. Is the code clean \u2014 no dead code, no TODOs left?",
5370
- "3. Does it follow existing patterns and conventions in the codebase?",
5371
- "4. Any regressions in the test suite?"
5372
- ]
5373
- };
5374
- }
5375
- if (roleLower === "cto" || roleLower.includes("architect")) {
5376
- return {
5377
- lens: "Architecture (CTO)",
5378
- checklist: [
5379
- "1. Does this fit the existing architecture? Consistent with ARCHITECTURE.md?",
5380
- "2. Is it backward compatible? Any breaking changes?",
5381
- "3. Does it introduce technical debt? Is that debt justified?",
5382
- "4. Security implications? Any new attack surface?",
5383
- "5. Does it scale? Performance considerations?",
5384
- "6. Coordination: does this affect other employees' work or other projects?"
5385
- ]
5386
- };
5387
- }
5388
- if (roleLower === "coo" || roleLower.includes("operations")) {
5389
- return {
5390
- lens: "Strategic (COO)",
5391
- checklist: [
5392
- "1. Does this serve the project mission?",
5393
- "2. Is this the right work at the right time?",
5394
- "3. Does the architectural assessment make sense for the business?",
5395
- "4. Any cross-project implications?"
5396
- ]
5397
- };
5398
- }
5399
- return {
5400
- lens: "General",
5401
- checklist: [
5402
- "1. Read the original task's acceptance criteria",
5403
- `2. Check git log for related commits: \`git log --oneline --author-date-order -10\``,
5404
- "3. Verify code changes match requirements",
5405
- "4. Check if tests were added/updated",
5406
- `5. Look for output files in exe/output/${agent}-${taskSlug}*`
5407
- ]
5408
- };
5409
- }
5410
- async function cleanupReviewFile(row, taskFile, _baseDir) {
5411
- if (String(row.assigned_by) !== "system" || !taskFile.includes("review-")) return;
5412
- try {
5413
- const client = getClient();
5414
- const now = (/* @__PURE__ */ new Date()).toISOString();
5415
- const parentId = row.parent_task_id ? String(row.parent_task_id) : null;
5416
- if (parentId) {
5417
- const result = await client.execute({
5418
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE id = ? AND status = 'needs_review'",
5419
- args: [now, parentId]
5420
- });
5421
- if (result.rowsAffected > 0) {
5422
- process.stderr.write(
5423
- `[review-cleanup] Cascaded original task to done via parent_task_id: ${parentId}
5424
- `
5425
- );
5426
- }
5427
- } else {
5428
- const fileName = taskFile.split("/").pop() ?? "";
5429
- const reviewPrefix = fileName.replace(".md", "");
5430
- const parts = reviewPrefix.split("-");
5431
- if (parts.length >= 3 && parts[0] === "review") {
5432
- const agent = parts[1];
5433
- const slug = parts.slice(2).join("-");
5434
- const legacyTaskFile = `exe/${agent}/${slug}.md`;
5435
- const result = await client.execute({
5436
- sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
5437
- args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
5438
- });
5439
- if (result.rowsAffected > 0) {
5440
- process.stderr.write(
5441
- `[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
5442
- `
5443
- );
5444
- }
5445
- }
5446
- }
5447
- } catch (err) {
5448
- process.stderr.write(
5449
- `[review-cleanup] Failed to cascade original task: ${err instanceof Error ? err.message : String(err)}
5450
- `
5451
- );
5452
- }
5453
- try {
5454
- const cacheDir = path15.join(EXE_AI_DIR, "session-cache");
5455
- if (existsSync14(cacheDir)) {
5456
- for (const f of readdirSync4(cacheDir)) {
5457
- if (f.startsWith("review-notified-")) {
5458
- unlinkSync4(path15.join(cacheDir, f));
5459
- }
5460
- }
5461
- }
5462
- } catch {
5463
- }
5464
- }
5465
- var init_tasks_review = __esm({
5466
- "src/lib/tasks-review.ts"() {
5467
- "use strict";
5468
- init_database();
5469
- init_config();
5470
- init_employees();
5471
- init_notifications();
5472
- init_tmux_routing();
5473
- init_session_key();
5474
- init_state_bus();
5475
- init_task_scope();
5476
- }
5477
- });
5478
-
5479
5615
  // src/lib/tasks-chain.ts
5480
5616
  import path16 from "path";
5481
5617
  import { readFile as readFile4, writeFile as writeFile5 } from "fs/promises";