@askexenow/exe-os 0.8.40 → 0.8.41

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.
package/dist/bin/cli.js CHANGED
@@ -31,6 +31,7 @@ var config_exports = {};
31
31
  __export(config_exports, {
32
32
  CONFIG_MIGRATIONS: () => CONFIG_MIGRATIONS,
33
33
  CONFIG_PATH: () => CONFIG_PATH,
34
+ COO_AGENT_NAME: () => COO_AGENT_NAME,
34
35
  CURRENT_CONFIG_VERSION: () => CURRENT_CONFIG_VERSION,
35
36
  DB_PATH: () => DB_PATH,
36
37
  EXE_AI_DIR: () => EXE_AI_DIR,
@@ -186,7 +187,7 @@ async function loadConfigFrom(configPath) {
186
187
  return { ...DEFAULT_CONFIG };
187
188
  }
188
189
  }
189
- var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
190
+ var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, COO_AGENT_NAME, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
190
191
  var init_config = __esm({
191
192
  "src/lib/config.ts"() {
192
193
  "use strict";
@@ -194,6 +195,7 @@ var init_config = __esm({
194
195
  DB_PATH = path.join(EXE_AI_DIR, "memories.db");
195
196
  MODELS_DIR = path.join(EXE_AI_DIR, "models");
196
197
  CONFIG_PATH = path.join(EXE_AI_DIR, "config.json");
198
+ COO_AGENT_NAME = "exe";
197
199
  LEGACY_LANCE_PATH = path.join(EXE_AI_DIR, "local.lance");
198
200
  CURRENT_CONFIG_VERSION = 1;
199
201
  DEFAULT_CONFIG = {
@@ -16901,7 +16903,7 @@ async function listTasks(input) {
16901
16903
  }
16902
16904
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
16903
16905
  const result = await client.execute({
16904
- sql: `SELECT * FROM tasks ${where} ORDER BY CASE status WHEN 'blocked' THEN 0 WHEN 'in_progress' THEN 1 WHEN 'open' THEN 2 ELSE 3 END, priority ASC, created_at DESC`,
16906
+ sql: `SELECT * FROM tasks ${where} ORDER BY CASE status WHEN 'blocked' THEN 0 WHEN 'in_progress' THEN 1 WHEN 'open' THEN 2 ELSE 3 END, priority ASC, created_at DESC LIMIT 1000`,
16905
16907
  args: args2
16906
16908
  });
16907
16909
  return result.rows.map((r) => ({
@@ -17122,6 +17124,34 @@ async function listPendingReviews(limit) {
17122
17124
  });
17123
17125
  return result.rows;
17124
17126
  }
17127
+ async function cleanupOrphanedReviews() {
17128
+ const client = getClient();
17129
+ const now = (/* @__PURE__ */ new Date()).toISOString();
17130
+ const r1 = await client.execute({
17131
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
17132
+ WHERE status = 'needs_review'
17133
+ AND assigned_by = 'system'
17134
+ AND title LIKE 'Review:%'
17135
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
17136
+ args: [now]
17137
+ });
17138
+ const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
17139
+ const r2 = await client.execute({
17140
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
17141
+ WHERE status = 'needs_review'
17142
+ AND result IS NOT NULL
17143
+ AND updated_at < ?`,
17144
+ args: [now, staleThreshold]
17145
+ });
17146
+ const total = r1.rowsAffected + r2.rowsAffected;
17147
+ if (total > 0) {
17148
+ process.stderr.write(
17149
+ `[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r2.rowsAffected} stale
17150
+ `
17151
+ );
17152
+ }
17153
+ return total;
17154
+ }
17125
17155
  function getReviewChecklist(role, agent, taskSlug) {
17126
17156
  const roleLower = role.toLowerCase();
17127
17157
  if (roleLower.includes("engineer") || roleLower === "principal engineer") {
@@ -17801,6 +17831,7 @@ var init_skill_learning = __esm({
17801
17831
  // src/lib/tasks.ts
17802
17832
  var tasks_exports = {};
17803
17833
  __export(tasks_exports, {
17834
+ cleanupOrphanedReviews: () => cleanupOrphanedReviews,
17804
17835
  countNewPendingReviewsSince: () => countNewPendingReviewsSince,
17805
17836
  countPendingReviews: () => countPendingReviews,
17806
17837
  createTask: () => createTask,
@@ -17866,6 +17897,21 @@ async function updateTask(input) {
17866
17897
  });
17867
17898
  } catch {
17868
17899
  }
17900
+ try {
17901
+ const client = getClient();
17902
+ const cascaded = await client.execute({
17903
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
17904
+ WHERE parent_task_id = ? AND status = 'needs_review'`,
17905
+ args: [now, taskId]
17906
+ });
17907
+ if (cascaded.rowsAffected > 0) {
17908
+ process.stderr.write(
17909
+ `[cascade] Closed ${cascaded.rowsAffected} orphaned review task(s) for parent ${taskId}
17910
+ `
17911
+ );
17912
+ }
17913
+ } catch {
17914
+ }
17869
17915
  }
17870
17916
  const isTerminal = input.status === "done" || input.status === "needs_review";
17871
17917
  if (isTerminal) {
@@ -31,6 +31,7 @@ var config_exports = {};
31
31
  __export(config_exports, {
32
32
  CONFIG_MIGRATIONS: () => CONFIG_MIGRATIONS,
33
33
  CONFIG_PATH: () => CONFIG_PATH,
34
+ COO_AGENT_NAME: () => COO_AGENT_NAME,
34
35
  CURRENT_CONFIG_VERSION: () => CURRENT_CONFIG_VERSION,
35
36
  DB_PATH: () => DB_PATH,
36
37
  EXE_AI_DIR: () => EXE_AI_DIR,
@@ -186,7 +187,7 @@ async function loadConfigFrom(configPath) {
186
187
  return { ...DEFAULT_CONFIG };
187
188
  }
188
189
  }
189
- var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
190
+ var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, COO_AGENT_NAME, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
190
191
  var init_config = __esm({
191
192
  "src/lib/config.ts"() {
192
193
  "use strict";
@@ -194,6 +195,7 @@ var init_config = __esm({
194
195
  DB_PATH = path.join(EXE_AI_DIR, "memories.db");
195
196
  MODELS_DIR = path.join(EXE_AI_DIR, "models");
196
197
  CONFIG_PATH = path.join(EXE_AI_DIR, "config.json");
198
+ COO_AGENT_NAME = "exe";
197
199
  LEGACY_LANCE_PATH = path.join(EXE_AI_DIR, "local.lance");
198
200
  CURRENT_CONFIG_VERSION = 1;
199
201
  DEFAULT_CONFIG = {
@@ -3588,7 +3590,7 @@ async function listTasks(input) {
3588
3590
  }
3589
3591
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3590
3592
  const result = await client.execute({
3591
- sql: `SELECT * FROM tasks ${where} ORDER BY CASE status WHEN 'blocked' THEN 0 WHEN 'in_progress' THEN 1 WHEN 'open' THEN 2 ELSE 3 END, priority ASC, created_at DESC`,
3593
+ sql: `SELECT * FROM tasks ${where} ORDER BY CASE status WHEN 'blocked' THEN 0 WHEN 'in_progress' THEN 1 WHEN 'open' THEN 2 ELSE 3 END, priority ASC, created_at DESC LIMIT 1000`,
3592
3594
  args
3593
3595
  });
3594
3596
  return result.rows.map((r) => ({
@@ -3809,6 +3811,34 @@ async function listPendingReviews(limit) {
3809
3811
  });
3810
3812
  return result.rows;
3811
3813
  }
3814
+ async function cleanupOrphanedReviews() {
3815
+ const client = getClient();
3816
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3817
+ const r1 = await client.execute({
3818
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
3819
+ WHERE status = 'needs_review'
3820
+ AND assigned_by = 'system'
3821
+ AND title LIKE 'Review:%'
3822
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
3823
+ args: [now]
3824
+ });
3825
+ const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
3826
+ const r2 = await client.execute({
3827
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
3828
+ WHERE status = 'needs_review'
3829
+ AND result IS NOT NULL
3830
+ AND updated_at < ?`,
3831
+ args: [now, staleThreshold]
3832
+ });
3833
+ const total = r1.rowsAffected + r2.rowsAffected;
3834
+ if (total > 0) {
3835
+ process.stderr.write(
3836
+ `[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r2.rowsAffected} stale
3837
+ `
3838
+ );
3839
+ }
3840
+ return total;
3841
+ }
3812
3842
  function getReviewChecklist(role, agent, taskSlug) {
3813
3843
  const roleLower = role.toLowerCase();
3814
3844
  if (roleLower.includes("engineer") || roleLower === "principal engineer") {
@@ -4446,6 +4476,7 @@ var init_skill_learning = __esm({
4446
4476
  // src/lib/tasks.ts
4447
4477
  var tasks_exports = {};
4448
4478
  __export(tasks_exports, {
4479
+ cleanupOrphanedReviews: () => cleanupOrphanedReviews,
4449
4480
  countNewPendingReviewsSince: () => countNewPendingReviewsSince,
4450
4481
  countPendingReviews: () => countPendingReviews,
4451
4482
  createTask: () => createTask,
@@ -4511,6 +4542,21 @@ async function updateTask(input) {
4511
4542
  });
4512
4543
  } catch {
4513
4544
  }
4545
+ try {
4546
+ const client = getClient();
4547
+ const cascaded = await client.execute({
4548
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
4549
+ WHERE parent_task_id = ? AND status = 'needs_review'`,
4550
+ args: [now, taskId]
4551
+ });
4552
+ if (cascaded.rowsAffected > 0) {
4553
+ process.stderr.write(
4554
+ `[cascade] Closed ${cascaded.rowsAffected} orphaned review task(s) for parent ${taskId}
4555
+ `
4556
+ );
4557
+ }
4558
+ } catch {
4559
+ }
4514
4560
  }
4515
4561
  const isTerminal = input.status === "done" || input.status === "needs_review";
4516
4562
  if (isTerminal) {
@@ -4809,7 +4855,8 @@ __export(cloud_sync_exports, {
4809
4855
  mergeRosterFromRemote: () => mergeRosterFromRemote,
4810
4856
  recordRosterDeletion: () => recordRosterDeletion
4811
4857
  });
4812
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync7, existsSync as existsSync14, readdirSync as readdirSync6, mkdirSync as mkdirSync8, appendFileSync as appendFileSync2, unlinkSync as unlinkSync6 } from "fs";
4858
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync7, existsSync as existsSync14, readdirSync as readdirSync6, mkdirSync as mkdirSync8, appendFileSync as appendFileSync2, unlinkSync as unlinkSync6, openSync, closeSync } from "fs";
4859
+ import crypto8 from "crypto";
4813
4860
  import path18 from "path";
4814
4861
  import { homedir } from "os";
4815
4862
  function logError(msg) {
@@ -4821,17 +4868,29 @@ function logError(msg) {
4821
4868
  }
4822
4869
  }
4823
4870
  async function withRosterLock(fn) {
4824
- if (existsSync14(ROSTER_LOCK_PATH)) {
4825
- try {
4826
- const ts = parseInt(readFileSync12(ROSTER_LOCK_PATH, "utf-8"), 10);
4827
- if (Date.now() - ts < LOCK_STALE_MS) {
4871
+ try {
4872
+ const fd = openSync(ROSTER_LOCK_PATH, "wx");
4873
+ closeSync(fd);
4874
+ writeFileSync7(ROSTER_LOCK_PATH, String(Date.now()));
4875
+ } catch (err) {
4876
+ if (err.code === "EEXIST") {
4877
+ try {
4878
+ const ts = parseInt(readFileSync12(ROSTER_LOCK_PATH, "utf-8"), 10);
4879
+ if (Date.now() - ts < LOCK_STALE_MS) {
4880
+ throw new Error("Roster merge already in progress \u2014 another sync is running");
4881
+ }
4882
+ unlinkSync6(ROSTER_LOCK_PATH);
4883
+ const fd = openSync(ROSTER_LOCK_PATH, "wx");
4884
+ closeSync(fd);
4885
+ writeFileSync7(ROSTER_LOCK_PATH, String(Date.now()));
4886
+ } catch (retryErr) {
4887
+ if (retryErr instanceof Error && retryErr.message.includes("already in progress")) throw retryErr;
4828
4888
  throw new Error("Roster merge already in progress \u2014 another sync is running");
4829
4889
  }
4830
- } catch (err) {
4831
- if (err instanceof Error && err.message.includes("already in progress")) throw err;
4890
+ } else {
4891
+ throw err;
4832
4892
  }
4833
4893
  }
4834
- writeFileSync7(ROSTER_LOCK_PATH, String(Date.now()));
4835
4894
  try {
4836
4895
  return await fn();
4837
4896
  } finally {
@@ -5153,7 +5212,7 @@ function buildRosterBlob(paths) {
5153
5212
  }
5154
5213
  const deletedNames = consumeRosterDeletions();
5155
5214
  const content = JSON.stringify({ roster, identities, config, deletedNames });
5156
- const hash = Buffer.from(content).length;
5215
+ const hash = crypto8.createHash("sha256").update(content).digest("hex").slice(0, 16);
5157
5216
  return { roster, identities, config, deletedNames, version: hash };
5158
5217
  }
5159
5218
  async function cloudPushRoster(config) {
@@ -5682,7 +5741,7 @@ __export(schedules_exports, {
5682
5741
  listSchedules: () => listSchedules,
5683
5742
  parseHumanCron: () => parseHumanCron
5684
5743
  });
5685
- import crypto8 from "crypto";
5744
+ import crypto9 from "crypto";
5686
5745
  import { execSync as execSync10 } from "child_process";
5687
5746
  async function ensureDb() {
5688
5747
  if (!isInitialized()) {
@@ -5751,7 +5810,7 @@ function parseHumanCron(input) {
5751
5810
  async function createSchedule(input) {
5752
5811
  await ensureDb();
5753
5812
  const client = getClient();
5754
- const id = crypto8.randomUUID().slice(0, 8);
5813
+ const id = crypto9.randomUUID().slice(0, 8);
5755
5814
  const now = (/* @__PURE__ */ new Date()).toISOString();
5756
5815
  const prompt = input.prompt ?? input.description;
5757
5816
  await client.execute({
@@ -7036,11 +7095,11 @@ async function boot(options) {
7036
7095
  const thisFile = fileURLToPath3(import.meta.url);
7037
7096
  const backfillPath = path19.resolve(path19.dirname(thisFile), "backfill-vectors.js");
7038
7097
  if (existsSync15(backfillPath)) {
7039
- const { openSync, closeSync } = await import("fs");
7098
+ const { openSync: openSync2, closeSync: closeSync2 } = await import("fs");
7040
7099
  const workerLogPath = path19.join(EXE_AI_DIR, "workers.log");
7041
7100
  let stderrFd = "ignore";
7042
7101
  try {
7043
- stderrFd = openSync(workerLogPath, "a");
7102
+ stderrFd = openSync2(workerLogPath, "a");
7044
7103
  } catch {
7045
7104
  }
7046
7105
  const child = spawn(process.execPath, [backfillPath], {
@@ -7049,7 +7108,7 @@ async function boot(options) {
7049
7108
  });
7050
7109
  child.unref();
7051
7110
  if (typeof stderrFd === "number") try {
7052
- closeSync(stderrFd);
7111
+ closeSync2(stderrFd);
7053
7112
  } catch {
7054
7113
  }
7055
7114
  briefData.embedding.backfillRunning = true;
@@ -710,7 +710,8 @@ function isMainModule(importMetaUrl) {
710
710
  }
711
711
 
712
712
  // src/lib/cloud-sync.ts
713
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync2, existsSync as existsSync6, readdirSync, mkdirSync as mkdirSync2, appendFileSync, unlinkSync } from "fs";
713
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync2, existsSync as existsSync6, readdirSync, mkdirSync as mkdirSync2, appendFileSync, unlinkSync, openSync, closeSync } from "fs";
714
+ import crypto4 from "crypto";
714
715
  import path6 from "path";
715
716
  import { homedir } from "os";
716
717
 
@@ -897,8 +898,17 @@ async function syncMode() {
897
898
  console.log("");
898
899
  console.log("Run /exe-cloud on the new device and paste these when prompted:\n");
899
900
  console.log(" 24-word recovery phrase:");
900
- console.log(` ${mnemonic}
901
+ const showFull = process.argv.includes("--show-full");
902
+ if (showFull) {
903
+ console.log(` ${mnemonic}
901
904
  `);
905
+ } else {
906
+ const words = mnemonic.split(" ");
907
+ const masked = words.length > 4 ? [...words.slice(0, 2), ...words.slice(2, -2).map(() => "****"), ...words.slice(-2)].join(" ") : mnemonic;
908
+ console.log(` ${masked}
909
+ `);
910
+ console.log(" To reveal full phrase: run with --show-full\n");
911
+ }
902
912
  console.log(" \u26A0 Clear your terminal history after copying.\n");
903
913
  if (config.cloud?.apiKey) {
904
914
  const maskedKey = config.cloud.apiKey.slice(0, 10) + "..." + config.cloud.apiKey.slice(-4);
@@ -1542,7 +1542,7 @@ async function auditDuplicates(client, flags) {
1542
1542
  if (flags.agent) filterClause += " AND agent_id = ?";
1543
1543
  if (flags.project) filterClause += " AND project_name = ?";
1544
1544
  const ids = await client.execute({
1545
- sql: `SELECT id FROM memories${filterClause} ORDER BY timestamp DESC`,
1545
+ sql: `SELECT id FROM memories${filterClause} ORDER BY timestamp DESC LIMIT 10000`,
1546
1546
  args: filterArgs
1547
1547
  });
1548
1548
  const allIds = ids.rows.map((r) => r.id);
@@ -1192,6 +1192,7 @@ var config_exports = {};
1192
1192
  __export(config_exports, {
1193
1193
  CONFIG_MIGRATIONS: () => CONFIG_MIGRATIONS,
1194
1194
  CONFIG_PATH: () => CONFIG_PATH,
1195
+ COO_AGENT_NAME: () => COO_AGENT_NAME,
1195
1196
  CURRENT_CONFIG_VERSION: () => CURRENT_CONFIG_VERSION,
1196
1197
  DB_PATH: () => DB_PATH,
1197
1198
  EXE_AI_DIR: () => EXE_AI_DIR,
@@ -1347,7 +1348,7 @@ async function loadConfigFrom(configPath) {
1347
1348
  return { ...DEFAULT_CONFIG };
1348
1349
  }
1349
1350
  }
1350
- var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
1351
+ var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, COO_AGENT_NAME, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
1351
1352
  var init_config = __esm({
1352
1353
  "src/lib/config.ts"() {
1353
1354
  "use strict";
@@ -1355,6 +1356,7 @@ var init_config = __esm({
1355
1356
  DB_PATH = path.join(EXE_AI_DIR, "memories.db");
1356
1357
  MODELS_DIR = path.join(EXE_AI_DIR, "models");
1357
1358
  CONFIG_PATH = path.join(EXE_AI_DIR, "config.json");
1359
+ COO_AGENT_NAME = "exe";
1358
1360
  LEGACY_LANCE_PATH = path.join(EXE_AI_DIR, "local.lance");
1359
1361
  CURRENT_CONFIG_VERSION = 1;
1360
1362
  DEFAULT_CONFIG = {
@@ -5849,7 +5851,7 @@ async function listTasks(input) {
5849
5851
  }
5850
5852
  const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
5851
5853
  const result = await client.execute({
5852
- sql: `SELECT * FROM tasks ${where} ORDER BY CASE status WHEN 'blocked' THEN 0 WHEN 'in_progress' THEN 1 WHEN 'open' THEN 2 ELSE 3 END, priority ASC, created_at DESC`,
5854
+ sql: `SELECT * FROM tasks ${where} ORDER BY CASE status WHEN 'blocked' THEN 0 WHEN 'in_progress' THEN 1 WHEN 'open' THEN 2 ELSE 3 END, priority ASC, created_at DESC LIMIT 1000`,
5853
5855
  args
5854
5856
  });
5855
5857
  return result.rows.map((r) => ({
@@ -6070,6 +6072,34 @@ async function listPendingReviews(limit) {
6070
6072
  });
6071
6073
  return result.rows;
6072
6074
  }
6075
+ async function cleanupOrphanedReviews() {
6076
+ const client = getClient();
6077
+ const now = (/* @__PURE__ */ new Date()).toISOString();
6078
+ const r1 = await client.execute({
6079
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
6080
+ WHERE status = 'needs_review'
6081
+ AND assigned_by = 'system'
6082
+ AND title LIKE 'Review:%'
6083
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
6084
+ args: [now]
6085
+ });
6086
+ const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
6087
+ const r2 = await client.execute({
6088
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
6089
+ WHERE status = 'needs_review'
6090
+ AND result IS NOT NULL
6091
+ AND updated_at < ?`,
6092
+ args: [now, staleThreshold]
6093
+ });
6094
+ const total = r1.rowsAffected + r2.rowsAffected;
6095
+ if (total > 0) {
6096
+ process.stderr.write(
6097
+ `[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r2.rowsAffected} stale
6098
+ `
6099
+ );
6100
+ }
6101
+ return total;
6102
+ }
6073
6103
  function getReviewChecklist(role, agent, taskSlug) {
6074
6104
  const roleLower = role.toLowerCase();
6075
6105
  if (roleLower.includes("engineer") || roleLower === "principal engineer") {
@@ -6749,6 +6779,7 @@ var init_skill_learning = __esm({
6749
6779
  // src/lib/tasks.ts
6750
6780
  var tasks_exports = {};
6751
6781
  __export(tasks_exports, {
6782
+ cleanupOrphanedReviews: () => cleanupOrphanedReviews,
6752
6783
  countNewPendingReviewsSince: () => countNewPendingReviewsSince,
6753
6784
  countPendingReviews: () => countPendingReviews,
6754
6785
  createTask: () => createTask,
@@ -6814,6 +6845,21 @@ async function updateTask(input) {
6814
6845
  });
6815
6846
  } catch {
6816
6847
  }
6848
+ try {
6849
+ const client = getClient();
6850
+ const cascaded = await client.execute({
6851
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
6852
+ WHERE parent_task_id = ? AND status = 'needs_review'`,
6853
+ args: [now, taskId]
6854
+ });
6855
+ if (cascaded.rowsAffected > 0) {
6856
+ process.stderr.write(
6857
+ `[cascade] Closed ${cascaded.rowsAffected} orphaned review task(s) for parent ${taskId}
6858
+ `
6859
+ );
6860
+ }
6861
+ } catch {
6862
+ }
6817
6863
  }
6818
6864
  const isTerminal = input.status === "done" || input.status === "needs_review";
6819
6865
  if (isTerminal) {
@@ -20,6 +20,7 @@ var config_exports = {};
20
20
  __export(config_exports, {
21
21
  CONFIG_MIGRATIONS: () => CONFIG_MIGRATIONS,
22
22
  CONFIG_PATH: () => CONFIG_PATH,
23
+ COO_AGENT_NAME: () => COO_AGENT_NAME,
23
24
  CURRENT_CONFIG_VERSION: () => CURRENT_CONFIG_VERSION,
24
25
  DB_PATH: () => DB_PATH,
25
26
  EXE_AI_DIR: () => EXE_AI_DIR,
@@ -175,7 +176,7 @@ async function loadConfigFrom(configPath) {
175
176
  return { ...DEFAULT_CONFIG };
176
177
  }
177
178
  }
178
- var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
179
+ var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, COO_AGENT_NAME, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
179
180
  var init_config = __esm({
180
181
  "src/lib/config.ts"() {
181
182
  "use strict";
@@ -183,6 +184,7 @@ var init_config = __esm({
183
184
  DB_PATH = path2.join(EXE_AI_DIR, "memories.db");
184
185
  MODELS_DIR = path2.join(EXE_AI_DIR, "models");
185
186
  CONFIG_PATH = path2.join(EXE_AI_DIR, "config.json");
187
+ COO_AGENT_NAME = "exe";
186
188
  LEGACY_LANCE_PATH = path2.join(EXE_AI_DIR, "local.lance");
187
189
  CURRENT_CONFIG_VERSION = 1;
188
190
  DEFAULT_CONFIG = {
@@ -671,7 +673,8 @@ __export(cloud_sync_exports, {
671
673
  mergeRosterFromRemote: () => mergeRosterFromRemote,
672
674
  recordRosterDeletion: () => recordRosterDeletion
673
675
  });
674
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync2, existsSync as existsSync6, readdirSync, mkdirSync as mkdirSync2, appendFileSync, unlinkSync } from "fs";
676
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync2, existsSync as existsSync6, readdirSync, mkdirSync as mkdirSync2, appendFileSync, unlinkSync, openSync, closeSync } from "fs";
677
+ import crypto3 from "crypto";
675
678
  import path6 from "path";
676
679
  import { homedir } from "os";
677
680
  function logError(msg) {
@@ -683,17 +686,29 @@ function logError(msg) {
683
686
  }
684
687
  }
685
688
  async function withRosterLock(fn) {
686
- if (existsSync6(ROSTER_LOCK_PATH)) {
687
- try {
688
- const ts = parseInt(readFileSync5(ROSTER_LOCK_PATH, "utf-8"), 10);
689
- if (Date.now() - ts < LOCK_STALE_MS) {
689
+ try {
690
+ const fd = openSync(ROSTER_LOCK_PATH, "wx");
691
+ closeSync(fd);
692
+ writeFileSync2(ROSTER_LOCK_PATH, String(Date.now()));
693
+ } catch (err) {
694
+ if (err.code === "EEXIST") {
695
+ try {
696
+ const ts = parseInt(readFileSync5(ROSTER_LOCK_PATH, "utf-8"), 10);
697
+ if (Date.now() - ts < LOCK_STALE_MS) {
698
+ throw new Error("Roster merge already in progress \u2014 another sync is running");
699
+ }
700
+ unlinkSync(ROSTER_LOCK_PATH);
701
+ const fd = openSync(ROSTER_LOCK_PATH, "wx");
702
+ closeSync(fd);
703
+ writeFileSync2(ROSTER_LOCK_PATH, String(Date.now()));
704
+ } catch (retryErr) {
705
+ if (retryErr instanceof Error && retryErr.message.includes("already in progress")) throw retryErr;
690
706
  throw new Error("Roster merge already in progress \u2014 another sync is running");
691
707
  }
692
- } catch (err) {
693
- if (err instanceof Error && err.message.includes("already in progress")) throw err;
708
+ } else {
709
+ throw err;
694
710
  }
695
711
  }
696
- writeFileSync2(ROSTER_LOCK_PATH, String(Date.now()));
697
712
  try {
698
713
  return await fn();
699
714
  } finally {
@@ -1015,7 +1030,7 @@ function buildRosterBlob(paths) {
1015
1030
  }
1016
1031
  const deletedNames = consumeRosterDeletions();
1017
1032
  const content = JSON.stringify({ roster, identities, config, deletedNames });
1018
- const hash = Buffer.from(content).length;
1033
+ const hash = crypto3.createHash("sha256").update(content).digest("hex").slice(0, 16);
1019
1034
  return { roster, identities, config, deletedNames, version: hash };
1020
1035
  }
1021
1036
  async function cloudPushRoster(config) {
@@ -1684,8 +1699,17 @@ async function main() {
1684
1699
  }
1685
1700
  const mnemonic = exportMnemonic(key);
1686
1701
  console.log("Your 24-word recovery phrase:\n");
1687
- console.log(` ${mnemonic}
1702
+ const showFull = process.argv.includes("--show-full");
1703
+ if (showFull) {
1704
+ console.log(` ${mnemonic}
1688
1705
  `);
1706
+ } else {
1707
+ const words = mnemonic.split(" ");
1708
+ const masked = words.length > 4 ? [...words.slice(0, 2), ...words.slice(2, -2).map(() => "****"), ...words.slice(-2)].join(" ") : mnemonic;
1709
+ console.log(` ${masked}
1710
+ `);
1711
+ console.log("To reveal full phrase: run with --show-full\n");
1712
+ }
1689
1713
  console.log("Write this down and enter it on your new device with /exe-link import.");
1690
1714
  console.log("Anyone with this phrase can decrypt your memories.");
1691
1715
  console.log("\u26A0 Clear your terminal history after copying.");
@@ -1509,6 +1509,34 @@ async function listPendingReviews(limit) {
1509
1509
  });
1510
1510
  return result.rows;
1511
1511
  }
1512
+ async function cleanupOrphanedReviews() {
1513
+ const client = getClient();
1514
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1515
+ const r1 = await client.execute({
1516
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
1517
+ WHERE status = 'needs_review'
1518
+ AND assigned_by = 'system'
1519
+ AND title LIKE 'Review:%'
1520
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
1521
+ args: [now]
1522
+ });
1523
+ const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
1524
+ const r2 = await client.execute({
1525
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
1526
+ WHERE status = 'needs_review'
1527
+ AND result IS NOT NULL
1528
+ AND updated_at < ?`,
1529
+ args: [now, staleThreshold]
1530
+ });
1531
+ const total = r1.rowsAffected + r2.rowsAffected;
1532
+ if (total > 0) {
1533
+ process.stderr.write(
1534
+ `[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r2.rowsAffected} stale
1535
+ `
1536
+ );
1537
+ }
1538
+ return total;
1539
+ }
1512
1540
  var init_tasks_review = __esm({
1513
1541
  "src/lib/tasks-review.ts"() {
1514
1542
  "use strict";
@@ -1662,6 +1690,7 @@ init_tasks_review();
1662
1690
  var PENDING_REVIEW_LIMIT = 10;
1663
1691
  async function main() {
1664
1692
  await initStore();
1693
+ await cleanupOrphanedReviews();
1665
1694
  const rows = await listPendingReviews(PENDING_REVIEW_LIMIT);
1666
1695
  if (rows.length > 0) {
1667
1696
  console.log("[REVIEW NOTIFICATIONS]");
@@ -969,6 +969,7 @@ var config_exports = {};
969
969
  __export(config_exports, {
970
970
  CONFIG_MIGRATIONS: () => CONFIG_MIGRATIONS,
971
971
  CONFIG_PATH: () => CONFIG_PATH,
972
+ COO_AGENT_NAME: () => COO_AGENT_NAME,
972
973
  CURRENT_CONFIG_VERSION: () => CURRENT_CONFIG_VERSION,
973
974
  DB_PATH: () => DB_PATH,
974
975
  EXE_AI_DIR: () => EXE_AI_DIR,
@@ -1124,7 +1125,7 @@ async function loadConfigFrom(configPath) {
1124
1125
  return { ...DEFAULT_CONFIG };
1125
1126
  }
1126
1127
  }
1127
- var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
1128
+ var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, COO_AGENT_NAME, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
1128
1129
  var init_config = __esm({
1129
1130
  "src/lib/config.ts"() {
1130
1131
  "use strict";
@@ -1132,6 +1133,7 @@ var init_config = __esm({
1132
1133
  DB_PATH = path2.join(EXE_AI_DIR, "memories.db");
1133
1134
  MODELS_DIR = path2.join(EXE_AI_DIR, "models");
1134
1135
  CONFIG_PATH = path2.join(EXE_AI_DIR, "config.json");
1136
+ COO_AGENT_NAME = "exe";
1135
1137
  LEGACY_LANCE_PATH = path2.join(EXE_AI_DIR, "local.lance");
1136
1138
  CURRENT_CONFIG_VERSION = 1;
1137
1139
  DEFAULT_CONFIG = {
@@ -922,6 +922,7 @@ var config_exports = {};
922
922
  __export(config_exports, {
923
923
  CONFIG_MIGRATIONS: () => CONFIG_MIGRATIONS,
924
924
  CONFIG_PATH: () => CONFIG_PATH,
925
+ COO_AGENT_NAME: () => COO_AGENT_NAME,
925
926
  CURRENT_CONFIG_VERSION: () => CURRENT_CONFIG_VERSION,
926
927
  DB_PATH: () => DB_PATH,
927
928
  EXE_AI_DIR: () => EXE_AI_DIR,
@@ -1077,7 +1078,7 @@ async function loadConfigFrom(configPath) {
1077
1078
  return { ...DEFAULT_CONFIG };
1078
1079
  }
1079
1080
  }
1080
- var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
1081
+ var EXE_AI_DIR, DB_PATH, MODELS_DIR, CONFIG_PATH, COO_AGENT_NAME, LEGACY_LANCE_PATH, CURRENT_CONFIG_VERSION, DEFAULT_CONFIG, CONFIG_MIGRATIONS;
1081
1082
  var init_config = __esm({
1082
1083
  "src/lib/config.ts"() {
1083
1084
  "use strict";
@@ -1085,6 +1086,7 @@ var init_config = __esm({
1085
1086
  DB_PATH = path2.join(EXE_AI_DIR, "memories.db");
1086
1087
  MODELS_DIR = path2.join(EXE_AI_DIR, "models");
1087
1088
  CONFIG_PATH = path2.join(EXE_AI_DIR, "config.json");
1089
+ COO_AGENT_NAME = "exe";
1088
1090
  LEGACY_LANCE_PATH = path2.join(EXE_AI_DIR, "local.lance");
1089
1091
  CURRENT_CONFIG_VERSION = 1;
1090
1092
  DEFAULT_CONFIG = {
@@ -2537,6 +2539,7 @@ var init_tmux_routing = __esm({
2537
2539
  // src/lib/tasks-review.ts
2538
2540
  var tasks_review_exports = {};
2539
2541
  __export(tasks_review_exports, {
2542
+ cleanupOrphanedReviews: () => cleanupOrphanedReviews,
2540
2543
  cleanupReviewFile: () => cleanupReviewFile,
2541
2544
  countNewPendingReviewsSince: () => countNewPendingReviewsSince,
2542
2545
  countPendingReviews: () => countPendingReviews,
@@ -2573,6 +2576,34 @@ async function listPendingReviews(limit) {
2573
2576
  });
2574
2577
  return result.rows;
2575
2578
  }
2579
+ async function cleanupOrphanedReviews() {
2580
+ const client = getClient();
2581
+ const now = (/* @__PURE__ */ new Date()).toISOString();
2582
+ const r1 = await client.execute({
2583
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
2584
+ WHERE status = 'needs_review'
2585
+ AND assigned_by = 'system'
2586
+ AND title LIKE 'Review:%'
2587
+ AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
2588
+ args: [now]
2589
+ });
2590
+ const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
2591
+ const r2 = await client.execute({
2592
+ sql: `UPDATE tasks SET status = 'done', updated_at = ?
2593
+ WHERE status = 'needs_review'
2594
+ AND result IS NOT NULL
2595
+ AND updated_at < ?`,
2596
+ args: [now, staleThreshold]
2597
+ });
2598
+ const total = r1.rowsAffected + r2.rowsAffected;
2599
+ if (total > 0) {
2600
+ process.stderr.write(
2601
+ `[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r2.rowsAffected} stale
2602
+ `
2603
+ );
2604
+ }
2605
+ return total;
2606
+ }
2576
2607
  function getReviewChecklist(role, agent, taskSlug) {
2577
2608
  const roleLower = role.toLowerCase();
2578
2609
  if (roleLower.includes("engineer") || roleLower === "principal engineer") {
@@ -208,7 +208,8 @@ function isMainModule(importMetaUrl) {
208
208
  }
209
209
 
210
210
  // src/lib/cloud-sync.ts
211
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync2, existsSync as existsSync5, readdirSync, mkdirSync as mkdirSync2, appendFileSync, unlinkSync } from "fs";
211
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync2, existsSync as existsSync5, readdirSync, mkdirSync as mkdirSync2, appendFileSync, unlinkSync, openSync, closeSync } from "fs";
212
+ import crypto2 from "crypto";
212
213
  import path5 from "path";
213
214
  import { homedir } from "os";
214
215