@askexenow/exe-os 0.8.32 → 0.8.36

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.
Files changed (87) hide show
  1. package/dist/bin/backfill-conversations.js +332 -348
  2. package/dist/bin/backfill-responses.js +72 -12
  3. package/dist/bin/backfill-vectors.js +72 -12
  4. package/dist/bin/cleanup-stale-review-tasks.js +63 -3
  5. package/dist/bin/cli.js +1518 -1122
  6. package/dist/bin/exe-agent.js +4 -4
  7. package/dist/bin/exe-assign.js +80 -18
  8. package/dist/bin/exe-boot.js +408 -89
  9. package/dist/bin/exe-call.js +83 -24
  10. package/dist/bin/exe-dispatch.js +18 -10
  11. package/dist/bin/exe-doctor.js +63 -3
  12. package/dist/bin/exe-export-behaviors.js +64 -3
  13. package/dist/bin/exe-forget.js +69 -4
  14. package/dist/bin/exe-gateway.js +121 -36
  15. package/dist/bin/exe-heartbeat.js +77 -13
  16. package/dist/bin/exe-kill.js +64 -3
  17. package/dist/bin/exe-launch-agent.js +162 -35
  18. package/dist/bin/exe-link.js +946 -0
  19. package/dist/bin/exe-new-employee.js +121 -36
  20. package/dist/bin/exe-pending-messages.js +72 -7
  21. package/dist/bin/exe-pending-notifications.js +63 -3
  22. package/dist/bin/exe-pending-reviews.js +75 -10
  23. package/dist/bin/exe-rename.js +1287 -0
  24. package/dist/bin/exe-review.js +64 -4
  25. package/dist/bin/exe-search.js +79 -13
  26. package/dist/bin/exe-session-cleanup.js +91 -26
  27. package/dist/bin/exe-status.js +64 -4
  28. package/dist/bin/exe-team.js +64 -4
  29. package/dist/bin/git-sweep.js +71 -4
  30. package/dist/bin/graph-backfill.js +64 -3
  31. package/dist/bin/graph-export.js +64 -3
  32. package/dist/bin/install.js +3 -3
  33. package/dist/bin/scan-tasks.js +71 -4
  34. package/dist/bin/setup.js +156 -38
  35. package/dist/bin/shard-migrate.js +64 -3
  36. package/dist/bin/wiki-sync.js +64 -3
  37. package/dist/gateway/index.js +122 -37
  38. package/dist/hooks/bug-report-worker.js +209 -23
  39. package/dist/hooks/commit-complete.js +71 -4
  40. package/dist/hooks/error-recall.js +79 -13
  41. package/dist/hooks/ingest-worker.js +129 -43
  42. package/dist/hooks/instructions-loaded.js +71 -4
  43. package/dist/hooks/notification.js +71 -4
  44. package/dist/hooks/post-compact.js +71 -4
  45. package/dist/hooks/pre-compact.js +71 -4
  46. package/dist/hooks/pre-tool-use.js +413 -194
  47. package/dist/hooks/prompt-ingest-worker.js +82 -22
  48. package/dist/hooks/prompt-submit.js +103 -37
  49. package/dist/hooks/response-ingest-worker.js +87 -22
  50. package/dist/hooks/session-end.js +71 -4
  51. package/dist/hooks/session-start.js +79 -13
  52. package/dist/hooks/stop.js +71 -4
  53. package/dist/hooks/subagent-stop.js +71 -4
  54. package/dist/hooks/summary-worker.js +303 -50
  55. package/dist/index.js +134 -46
  56. package/dist/lib/cloud-sync.js +209 -15
  57. package/dist/lib/consolidation.js +4 -4
  58. package/dist/lib/database.js +64 -2
  59. package/dist/lib/device-registry.js +70 -3
  60. package/dist/lib/employee-templates.js +48 -22
  61. package/dist/lib/employees.js +34 -1
  62. package/dist/lib/exe-daemon.js +136 -53
  63. package/dist/lib/hybrid-search.js +79 -13
  64. package/dist/lib/identity-templates.js +57 -6
  65. package/dist/lib/identity.js +3 -3
  66. package/dist/lib/messaging.js +22 -14
  67. package/dist/lib/reminders.js +3 -3
  68. package/dist/lib/schedules.js +63 -3
  69. package/dist/lib/skill-learning.js +3 -3
  70. package/dist/lib/status-brief.js +63 -5
  71. package/dist/lib/store.js +64 -3
  72. package/dist/lib/task-router.js +4 -2
  73. package/dist/lib/tasks.js +48 -21
  74. package/dist/lib/tmux-routing.js +47 -20
  75. package/dist/mcp/server.js +727 -58
  76. package/dist/mcp/tools/complete-reminder.js +3 -3
  77. package/dist/mcp/tools/create-reminder.js +3 -3
  78. package/dist/mcp/tools/create-task.js +151 -24
  79. package/dist/mcp/tools/deactivate-behavior.js +3 -3
  80. package/dist/mcp/tools/list-reminders.js +3 -3
  81. package/dist/mcp/tools/list-tasks.js +17 -8
  82. package/dist/mcp/tools/send-message.js +24 -16
  83. package/dist/mcp/tools/update-task.js +25 -16
  84. package/dist/runtime/index.js +112 -24
  85. package/dist/tui/App.js +139 -36
  86. package/package.json +6 -2
  87. package/src/commands/exe/rename.md +12 -0
@@ -203,12 +203,58 @@ var init_config = __esm({
203
203
  }
204
204
  });
205
205
 
206
- // src/types/memory.ts
207
- var EMBEDDING_DIM;
208
- var init_memory = __esm({
209
- "src/types/memory.ts"() {
206
+ // src/lib/db-retry.ts
207
+ function isBusyError(err) {
208
+ if (err instanceof Error) {
209
+ const msg = err.message.toLowerCase();
210
+ return msg.includes("sqlite_busy") || msg.includes("database is locked");
211
+ }
212
+ return false;
213
+ }
214
+ function delay(ms) {
215
+ return new Promise((resolve) => setTimeout(resolve, ms));
216
+ }
217
+ async function retryOnBusy(fn, label) {
218
+ let lastError;
219
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
220
+ try {
221
+ return await fn();
222
+ } catch (err) {
223
+ lastError = err;
224
+ if (!isBusyError(err) || attempt === MAX_RETRIES) {
225
+ throw err;
226
+ }
227
+ const backoff = BASE_DELAY_MS * Math.pow(2, attempt);
228
+ const jitter = Math.floor(Math.random() * MAX_JITTER_MS);
229
+ process.stderr.write(
230
+ `[exe-os] SQLITE_BUSY ${label} retry ${attempt + 1}/${MAX_RETRIES} \u2014 waiting ${backoff + jitter}ms
231
+ `
232
+ );
233
+ await delay(backoff + jitter);
234
+ }
235
+ }
236
+ throw lastError;
237
+ }
238
+ function wrapWithRetry(client) {
239
+ return new Proxy(client, {
240
+ get(target, prop, receiver) {
241
+ if (prop === "execute") {
242
+ return (sql) => retryOnBusy(() => target.execute(sql), "execute");
243
+ }
244
+ if (prop === "batch") {
245
+ return (stmts) => retryOnBusy(() => target.batch(stmts), "batch");
246
+ }
247
+ return Reflect.get(target, prop, receiver);
248
+ }
249
+ });
250
+ }
251
+ var MAX_RETRIES, BASE_DELAY_MS, MAX_JITTER_MS;
252
+ var init_db_retry = __esm({
253
+ "src/lib/db-retry.ts"() {
210
254
  "use strict";
211
- EMBEDDING_DIM = 1024;
255
+ MAX_RETRIES = 3;
256
+ BASE_DELAY_MS = 200;
257
+ MAX_JITTER_MS = 300;
212
258
  }
213
259
  });
214
260
 
@@ -219,6 +265,7 @@ __export(database_exports, {
219
265
  disposeTurso: () => disposeTurso,
220
266
  ensureSchema: () => ensureSchema,
221
267
  getClient: () => getClient,
268
+ getRawClient: () => getRawClient,
222
269
  initDatabase: () => initDatabase,
223
270
  initTurso: () => initTurso,
224
271
  isInitialized: () => isInitialized
@@ -228,6 +275,7 @@ async function initDatabase(config) {
228
275
  if (_client) {
229
276
  _client.close();
230
277
  _client = null;
278
+ _resilientClient = null;
231
279
  }
232
280
  const opts = {
233
281
  url: `file:${config.dbPath}`
@@ -236,20 +284,27 @@ async function initDatabase(config) {
236
284
  opts.encryptionKey = config.encryptionKey;
237
285
  }
238
286
  _client = createClient(opts);
287
+ _resilientClient = wrapWithRetry(_client);
239
288
  }
240
289
  function isInitialized() {
241
290
  return _client !== null;
242
291
  }
243
292
  function getClient() {
293
+ if (!_resilientClient) {
294
+ throw new Error("Database client not initialized. Call initDatabase() first.");
295
+ }
296
+ return _resilientClient;
297
+ }
298
+ function getRawClient() {
244
299
  if (!_client) {
245
300
  throw new Error("Database client not initialized. Call initDatabase() first.");
246
301
  }
247
302
  return _client;
248
303
  }
249
304
  async function ensureSchema() {
250
- const client = getClient();
305
+ const client = getRawClient();
251
306
  await client.execute("PRAGMA journal_mode = WAL");
252
- await client.execute("PRAGMA busy_timeout = 5000");
307
+ await client.execute("PRAGMA busy_timeout = 30000");
253
308
  try {
254
309
  await client.execute("PRAGMA libsql_vector_search_ef = 128");
255
310
  } catch {
@@ -1042,28 +1097,344 @@ async function disposeDatabase() {
1042
1097
  if (_client) {
1043
1098
  _client.close();
1044
1099
  _client = null;
1100
+ _resilientClient = null;
1045
1101
  }
1046
1102
  }
1047
- var _client, initTurso, disposeTurso;
1103
+ var _client, _resilientClient, initTurso, disposeTurso;
1048
1104
  var init_database = __esm({
1049
1105
  "src/lib/database.ts"() {
1050
1106
  "use strict";
1107
+ init_db_retry();
1051
1108
  _client = null;
1109
+ _resilientClient = null;
1052
1110
  initTurso = initDatabase;
1053
1111
  disposeTurso = disposeDatabase;
1054
1112
  }
1055
1113
  });
1056
1114
 
1057
- // src/lib/keychain.ts
1058
- import { readFile as readFile2, writeFile as writeFile2, unlink, mkdir as mkdir2, chmod } from "fs/promises";
1059
- import { existsSync as existsSync2 } from "fs";
1115
+ // src/lib/employees.ts
1116
+ var employees_exports = {};
1117
+ __export(employees_exports, {
1118
+ EMPLOYEES_PATH: () => EMPLOYEES_PATH,
1119
+ addEmployee: () => addEmployee,
1120
+ getEmployee: () => getEmployee,
1121
+ getEmployeeByRole: () => getEmployeeByRole,
1122
+ getEmployeeNamesByRole: () => getEmployeeNamesByRole,
1123
+ hasRole: () => hasRole,
1124
+ isMultiInstance: () => isMultiInstance,
1125
+ loadEmployees: () => loadEmployees,
1126
+ loadEmployeesSync: () => loadEmployeesSync,
1127
+ registerBinSymlinks: () => registerBinSymlinks,
1128
+ saveEmployees: () => saveEmployees,
1129
+ validateEmployeeName: () => validateEmployeeName
1130
+ });
1131
+ import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
1132
+ import { existsSync as existsSync2, symlinkSync, readlinkSync, readFileSync as readFileSync3 } from "fs";
1133
+ import { execSync as execSync3 } from "child_process";
1060
1134
  import path3 from "path";
1135
+ function validateEmployeeName(name) {
1136
+ if (!name) {
1137
+ return { valid: false, error: "Name is required" };
1138
+ }
1139
+ if (name.length > 32) {
1140
+ return { valid: false, error: "Name must be 32 characters or fewer" };
1141
+ }
1142
+ if (!/^[a-z][a-z0-9]*$/.test(name)) {
1143
+ return {
1144
+ valid: false,
1145
+ error: "Name must start with a letter and contain only lowercase alphanumeric characters"
1146
+ };
1147
+ }
1148
+ return { valid: true };
1149
+ }
1150
+ async function loadEmployees(employeesPath = EMPLOYEES_PATH) {
1151
+ if (!existsSync2(employeesPath)) {
1152
+ return [];
1153
+ }
1154
+ const raw = await readFile2(employeesPath, "utf-8");
1155
+ try {
1156
+ return JSON.parse(raw);
1157
+ } catch {
1158
+ return [];
1159
+ }
1160
+ }
1161
+ async function saveEmployees(employees, employeesPath = EMPLOYEES_PATH) {
1162
+ await mkdir2(path3.dirname(employeesPath), { recursive: true });
1163
+ await writeFile2(employeesPath, JSON.stringify(employees, null, 2) + "\n", "utf-8");
1164
+ }
1165
+ function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
1166
+ if (!existsSync2(employeesPath)) return [];
1167
+ try {
1168
+ return JSON.parse(readFileSync3(employeesPath, "utf-8"));
1169
+ } catch {
1170
+ return [];
1171
+ }
1172
+ }
1173
+ function getEmployee(employees, name) {
1174
+ return employees.find((e) => e.name.toLowerCase() === name.toLowerCase());
1175
+ }
1176
+ function getEmployeeByRole(employees, role) {
1177
+ const lower = role.toLowerCase();
1178
+ return employees.find((e) => e.role.toLowerCase() === lower);
1179
+ }
1180
+ function getEmployeeNamesByRole(employees, role) {
1181
+ const lower = role.toLowerCase();
1182
+ return employees.filter((e) => e.role.toLowerCase() === lower).map((e) => e.name);
1183
+ }
1184
+ function hasRole(agentName, role) {
1185
+ const employees = loadEmployeesSync();
1186
+ const emp = getEmployee(employees, agentName);
1187
+ return emp ? emp.role.toLowerCase() === role.toLowerCase() : false;
1188
+ }
1189
+ function isMultiInstance(agentName, employees) {
1190
+ const roster = employees ?? loadEmployeesSync();
1191
+ const emp = getEmployee(roster, agentName);
1192
+ if (!emp) return false;
1193
+ return MULTI_INSTANCE_ROLES.has(emp.role.toLowerCase());
1194
+ }
1195
+ function addEmployee(employees, employee) {
1196
+ const normalized = { ...employee, name: employee.name.toLowerCase() };
1197
+ if (employees.some((e) => e.name.toLowerCase() === normalized.name)) {
1198
+ throw new Error(`Employee '${normalized.name}' already exists`);
1199
+ }
1200
+ return [...employees, normalized];
1201
+ }
1202
+ function registerBinSymlinks(name) {
1203
+ const created = [];
1204
+ const skipped = [];
1205
+ const errors = [];
1206
+ let exeBinPath;
1207
+ try {
1208
+ exeBinPath = execSync3("which exe", { encoding: "utf-8" }).trim();
1209
+ } catch {
1210
+ errors.push("Could not find 'exe' in PATH");
1211
+ return { created, skipped, errors };
1212
+ }
1213
+ const binDir = path3.dirname(exeBinPath);
1214
+ let target;
1215
+ try {
1216
+ target = readlinkSync(exeBinPath);
1217
+ } catch {
1218
+ errors.push("Could not read 'exe' symlink");
1219
+ return { created, skipped, errors };
1220
+ }
1221
+ for (const suffix of ["", "-opencode"]) {
1222
+ const linkName = `${name}${suffix}`;
1223
+ const linkPath = path3.join(binDir, linkName);
1224
+ if (existsSync2(linkPath)) {
1225
+ skipped.push(linkName);
1226
+ continue;
1227
+ }
1228
+ try {
1229
+ symlinkSync(target, linkPath);
1230
+ created.push(linkName);
1231
+ } catch (err) {
1232
+ errors.push(`${linkName}: ${err instanceof Error ? err.message : String(err)}`);
1233
+ }
1234
+ }
1235
+ return { created, skipped, errors };
1236
+ }
1237
+ var EMPLOYEES_PATH, MULTI_INSTANCE_ROLES;
1238
+ var init_employees = __esm({
1239
+ "src/lib/employees.ts"() {
1240
+ "use strict";
1241
+ init_config();
1242
+ EMPLOYEES_PATH = path3.join(EXE_AI_DIR, "exe-employees.json");
1243
+ MULTI_INSTANCE_ROLES = /* @__PURE__ */ new Set(["principal engineer", "content production specialist", "staff code reviewer"]);
1244
+ }
1245
+ });
1246
+
1247
+ // src/lib/yoshi-delegation-gate.ts
1248
+ var yoshi_delegation_gate_exports = {};
1249
+ __export(yoshi_delegation_gate_exports, {
1250
+ GATED_AGENT: () => GATED_AGENT,
1251
+ GATED_ROLE: () => GATED_ROLE,
1252
+ GATE_BLOCK_MESSAGE: () => GATE_BLOCK_MESSAGE,
1253
+ classifyPath: () => classifyPath,
1254
+ evaluateDelegationGate: () => evaluateDelegationGate,
1255
+ hasRecentTomDispatch: () => hasRecentTomDispatch,
1256
+ hasValidScratchpadEscape: () => hasValidScratchpadEscape,
1257
+ scratchpadPath: () => scratchpadPath
1258
+ });
1259
+ import os2 from "os";
1260
+ import path4 from "path";
1261
+ import { existsSync as existsSync3, readFileSync as readFileSync4, statSync } from "fs";
1262
+ function resolveGatedAgent() {
1263
+ try {
1264
+ const employees = loadEmployeesSync();
1265
+ const cto = getEmployeeByRole(employees, GATED_ROLE);
1266
+ return cto?.name ?? "yoshi";
1267
+ } catch {
1268
+ return "yoshi";
1269
+ }
1270
+ }
1271
+ function toWorkspaceRelative(filePath, cwd = process.cwd()) {
1272
+ const normalized = path4.normalize(filePath);
1273
+ if (normalized.startsWith(cwd + path4.sep)) {
1274
+ return normalized.slice(cwd.length + 1);
1275
+ }
1276
+ if (path4.isAbsolute(normalized)) {
1277
+ return path4.basename(normalized);
1278
+ }
1279
+ return normalized;
1280
+ }
1281
+ function isDockerfile(basename) {
1282
+ return basename === BLOCKED_DOCKERFILE_PREFIX || basename.startsWith(`${BLOCKED_DOCKERFILE_PREFIX}.`);
1283
+ }
1284
+ function classifyPath(filePath, cwd) {
1285
+ const rel = toWorkspaceRelative(filePath, cwd);
1286
+ const basename = path4.basename(rel);
1287
+ const ext = path4.extname(rel).toLowerCase();
1288
+ if (ext === ".md") {
1289
+ for (const prefix of EXEMPT_MD_DIR_PREFIXES) {
1290
+ if (rel.startsWith(prefix)) return "exempt";
1291
+ }
1292
+ if (!BLOCKED_PATH_PREFIXES.some((p) => rel.startsWith(p))) {
1293
+ return "exempt";
1294
+ }
1295
+ }
1296
+ if (BLOCKED_FILENAMES.includes(basename)) return "code";
1297
+ if (isDockerfile(basename)) return "code";
1298
+ if (BLOCKED_EXTENSIONS.includes(ext)) return "code";
1299
+ if (BLOCKED_PATH_PREFIXES.some((p) => rel.startsWith(p))) return "code";
1300
+ return "exempt";
1301
+ }
1302
+ async function hasRecentTomDispatch(now = Date.now()) {
1303
+ try {
1304
+ const client = getClient();
1305
+ const engineers = loadEmployeesSync().filter(
1306
+ (e) => e.role.toLowerCase() === "principal engineer"
1307
+ );
1308
+ const engineerNames = engineers.map((e) => e.name);
1309
+ if (engineerNames.length === 0) return false;
1310
+ const placeholders = engineerNames.map(() => "?").join(",");
1311
+ const result = await client.execute({
1312
+ sql: `SELECT MAX(created_at) AS last_dispatch
1313
+ FROM tasks
1314
+ WHERE assigned_by = ? AND assigned_to IN (${placeholders})`,
1315
+ args: [GATED_AGENT, ...engineerNames]
1316
+ });
1317
+ const raw = result.rows[0]?.last_dispatch;
1318
+ if (raw === null || raw === void 0) return false;
1319
+ const parsed = Date.parse(String(raw));
1320
+ if (Number.isNaN(parsed)) return false;
1321
+ return now - parsed <= RECENT_DISPATCH_WINDOW_MS;
1322
+ } catch {
1323
+ return false;
1324
+ }
1325
+ }
1326
+ function scratchpadPath(sessionId, homeDir = os2.homedir()) {
1327
+ return path4.join(
1328
+ homeDir,
1329
+ ".exe-os",
1330
+ "session-cache",
1331
+ `yoshi-scratchpad-${sessionId}.txt`
1332
+ );
1333
+ }
1334
+ function hasValidScratchpadEscape(sessionId, now = Date.now(), homeDir = os2.homedir()) {
1335
+ const filePath = scratchpadPath(sessionId, homeDir);
1336
+ if (!existsSync3(filePath)) return false;
1337
+ try {
1338
+ const stat = statSync(filePath);
1339
+ if (now - stat.mtimeMs > SCRATCHPAD_MAX_AGE_MS) return false;
1340
+ const body = readFileSync4(filePath, "utf-8");
1341
+ return SCRATCHPAD_ESCAPE_PATTERN.test(body);
1342
+ } catch {
1343
+ return false;
1344
+ }
1345
+ }
1346
+ async function evaluateDelegationGate(opts) {
1347
+ if (opts.agentId !== GATED_AGENT) {
1348
+ return { outcome: "allow", reason: "agent_not_gated" };
1349
+ }
1350
+ if (!/^(Write|Edit)$/.test(opts.toolName)) {
1351
+ return { outcome: "allow", reason: "tool_not_gated" };
1352
+ }
1353
+ if (classifyPath(opts.filePath, opts.cwd) === "exempt") {
1354
+ return { outcome: "allow", reason: "exempt_path" };
1355
+ }
1356
+ if (await hasRecentTomDispatch(opts.now)) {
1357
+ return { outcome: "allow", reason: "recent_tom_dispatch" };
1358
+ }
1359
+ if (hasValidScratchpadEscape(opts.sessionId, opts.now, opts.homeDir)) {
1360
+ return { outcome: "allow", reason: "scratchpad_escape" };
1361
+ }
1362
+ return { outcome: "block", reason: "no_delegation_no_escape" };
1363
+ }
1364
+ var GATED_ROLE, GATED_AGENT, BLOCKED_PATH_PREFIXES, BLOCKED_FILENAMES, BLOCKED_DOCKERFILE_PREFIX, BLOCKED_EXTENSIONS, EXEMPT_MD_DIR_PREFIXES, RECENT_DISPATCH_WINDOW_MS, SCRATCHPAD_MAX_AGE_MS, SCRATCHPAD_ESCAPE_PATTERN, GATE_BLOCK_MESSAGE;
1365
+ var init_yoshi_delegation_gate = __esm({
1366
+ "src/lib/yoshi-delegation-gate.ts"() {
1367
+ "use strict";
1368
+ init_database();
1369
+ init_employees();
1370
+ GATED_ROLE = "CTO";
1371
+ GATED_AGENT = resolveGatedAgent();
1372
+ BLOCKED_PATH_PREFIXES = [
1373
+ "src/",
1374
+ "tests/",
1375
+ "scripts/",
1376
+ "bin/",
1377
+ ".github/workflows/",
1378
+ ".claude/skills/"
1379
+ ];
1380
+ BLOCKED_FILENAMES = [
1381
+ "package.json",
1382
+ "tsconfig.json",
1383
+ "docker-compose.yml"
1384
+ ];
1385
+ BLOCKED_DOCKERFILE_PREFIX = "Dockerfile";
1386
+ BLOCKED_EXTENSIONS = [
1387
+ ".ts",
1388
+ ".js",
1389
+ ".tsx",
1390
+ ".jsx",
1391
+ ".rs",
1392
+ ".py",
1393
+ ".go",
1394
+ ".sh",
1395
+ ".yaml",
1396
+ ".yml",
1397
+ ".prisma",
1398
+ ".sql"
1399
+ ];
1400
+ EXEMPT_MD_DIR_PREFIXES = [
1401
+ ".planning/",
1402
+ "exe/output/",
1403
+ "exe/yoshi/",
1404
+ "exe/tom/",
1405
+ "exe/",
1406
+ "docs/"
1407
+ ];
1408
+ RECENT_DISPATCH_WINDOW_MS = 5 * 60 * 1e3;
1409
+ SCRATCHPAD_MAX_AGE_MS = 10 * 60 * 1e3;
1410
+ SCRATCHPAD_ESCAPE_PATTERN = /^TOM-CAPABLE:\s*NO\s*—\s*reason:\s*\S+/m;
1411
+ GATE_BLOCK_MESSAGE = [
1412
+ "\u26A0\uFE0F DELEGATE CHECK: this edit looks implementable by Tom.",
1413
+ "Dispatch create_task now OR mark TOM-CAPABLE: NO with reason.",
1414
+ "No code until decided."
1415
+ ].join("\n");
1416
+ }
1417
+ });
1418
+
1419
+ // src/types/memory.ts
1420
+ var EMBEDDING_DIM;
1421
+ var init_memory = __esm({
1422
+ "src/types/memory.ts"() {
1423
+ "use strict";
1424
+ EMBEDDING_DIM = 1024;
1425
+ }
1426
+ });
1427
+
1428
+ // src/lib/keychain.ts
1429
+ import { readFile as readFile3, writeFile as writeFile3, unlink, mkdir as mkdir3, chmod } from "fs/promises";
1430
+ import { existsSync as existsSync4 } from "fs";
1431
+ import path5 from "path";
1061
1432
  import crypto from "crypto";
1062
1433
  function getKeyDir() {
1063
- return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path3.join(process.env.HOME ?? "/tmp", ".exe-os");
1434
+ return process.env.EXE_OS_DIR ?? process.env.EXE_MEM_DIR ?? path5.join(process.env.HOME ?? "/tmp", ".exe-os");
1064
1435
  }
1065
1436
  function getKeyPath() {
1066
- return path3.join(getKeyDir(), "master.key");
1437
+ return path5.join(getKeyDir(), "master.key");
1067
1438
  }
1068
1439
  async function tryKeytar() {
1069
1440
  try {
@@ -1084,11 +1455,11 @@ async function getMasterKey() {
1084
1455
  }
1085
1456
  }
1086
1457
  const keyPath = getKeyPath();
1087
- if (!existsSync2(keyPath)) {
1458
+ if (!existsSync4(keyPath)) {
1088
1459
  return null;
1089
1460
  }
1090
1461
  try {
1091
- const content = await readFile2(keyPath, "utf-8");
1462
+ const content = await readFile3(keyPath, "utf-8");
1092
1463
  return Buffer.from(content.trim(), "base64");
1093
1464
  } catch {
1094
1465
  return null;
@@ -1116,12 +1487,12 @@ __export(shard_manager_exports, {
1116
1487
  listShards: () => listShards,
1117
1488
  shardExists: () => shardExists
1118
1489
  });
1119
- import path4 from "path";
1120
- import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
1490
+ import path6 from "path";
1491
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
1121
1492
  import { createClient as createClient2 } from "@libsql/client";
1122
1493
  function initShardManager(encryptionKey) {
1123
1494
  _encryptionKey = encryptionKey;
1124
- if (!existsSync3(SHARDS_DIR)) {
1495
+ if (!existsSync5(SHARDS_DIR)) {
1125
1496
  mkdirSync2(SHARDS_DIR, { recursive: true });
1126
1497
  }
1127
1498
  _shardingEnabled = true;
@@ -1142,7 +1513,7 @@ function getShardClient(projectName) {
1142
1513
  }
1143
1514
  const cached = _shards.get(safeName);
1144
1515
  if (cached) return cached;
1145
- const dbPath = path4.join(SHARDS_DIR, `${safeName}.db`);
1516
+ const dbPath = path6.join(SHARDS_DIR, `${safeName}.db`);
1146
1517
  const client = createClient2({
1147
1518
  url: `file:${dbPath}`,
1148
1519
  encryptionKey: _encryptionKey
@@ -1152,16 +1523,16 @@ function getShardClient(projectName) {
1152
1523
  }
1153
1524
  function shardExists(projectName) {
1154
1525
  const safeName = projectName.replace(/[^a-zA-Z0-9_-]/g, "_");
1155
- return existsSync3(path4.join(SHARDS_DIR, `${safeName}.db`));
1526
+ return existsSync5(path6.join(SHARDS_DIR, `${safeName}.db`));
1156
1527
  }
1157
1528
  function listShards() {
1158
- if (!existsSync3(SHARDS_DIR)) return [];
1529
+ if (!existsSync5(SHARDS_DIR)) return [];
1159
1530
  const { readdirSync: readdirSync2 } = __require("fs");
1160
1531
  return readdirSync2(SHARDS_DIR).filter((f) => f.endsWith(".db")).map((f) => f.replace(".db", ""));
1161
1532
  }
1162
1533
  async function ensureShardSchema(client) {
1163
1534
  await client.execute("PRAGMA journal_mode = WAL");
1164
- await client.execute("PRAGMA busy_timeout = 5000");
1535
+ await client.execute("PRAGMA busy_timeout = 30000");
1165
1536
  try {
1166
1537
  await client.execute("PRAGMA libsql_vector_search_ef = 128");
1167
1538
  } catch {
@@ -1338,7 +1709,7 @@ var init_shard_manager = __esm({
1338
1709
  "src/lib/shard-manager.ts"() {
1339
1710
  "use strict";
1340
1711
  init_config();
1341
- SHARDS_DIR = path4.join(EXE_AI_DIR, "shards");
1712
+ SHARDS_DIR = path6.join(EXE_AI_DIR, "shards");
1342
1713
  _shards = /* @__PURE__ */ new Map();
1343
1714
  _encryptionKey = null;
1344
1715
  _shardingEnabled = false;
@@ -1780,160 +2151,6 @@ var init_store = __esm({
1780
2151
  }
1781
2152
  });
1782
2153
 
1783
- // src/lib/yoshi-delegation-gate.ts
1784
- var yoshi_delegation_gate_exports = {};
1785
- __export(yoshi_delegation_gate_exports, {
1786
- GATED_AGENT: () => GATED_AGENT,
1787
- GATE_BLOCK_MESSAGE: () => GATE_BLOCK_MESSAGE,
1788
- classifyPath: () => classifyPath,
1789
- evaluateDelegationGate: () => evaluateDelegationGate,
1790
- hasRecentTomDispatch: () => hasRecentTomDispatch,
1791
- hasValidScratchpadEscape: () => hasValidScratchpadEscape,
1792
- scratchpadPath: () => scratchpadPath
1793
- });
1794
- import os2 from "os";
1795
- import path5 from "path";
1796
- import { existsSync as existsSync4, readFileSync as readFileSync3, statSync } from "fs";
1797
- function toWorkspaceRelative(filePath, cwd = process.cwd()) {
1798
- const normalized = path5.normalize(filePath);
1799
- if (normalized.startsWith(cwd + path5.sep)) {
1800
- return normalized.slice(cwd.length + 1);
1801
- }
1802
- if (path5.isAbsolute(normalized)) {
1803
- return path5.basename(normalized);
1804
- }
1805
- return normalized;
1806
- }
1807
- function isDockerfile(basename) {
1808
- return basename === BLOCKED_DOCKERFILE_PREFIX || basename.startsWith(`${BLOCKED_DOCKERFILE_PREFIX}.`);
1809
- }
1810
- function classifyPath(filePath, cwd) {
1811
- const rel = toWorkspaceRelative(filePath, cwd);
1812
- const basename = path5.basename(rel);
1813
- const ext = path5.extname(rel).toLowerCase();
1814
- if (ext === ".md") {
1815
- for (const prefix of EXEMPT_MD_DIR_PREFIXES) {
1816
- if (rel.startsWith(prefix)) return "exempt";
1817
- }
1818
- if (!BLOCKED_PATH_PREFIXES.some((p) => rel.startsWith(p))) {
1819
- return "exempt";
1820
- }
1821
- }
1822
- if (BLOCKED_FILENAMES.includes(basename)) return "code";
1823
- if (isDockerfile(basename)) return "code";
1824
- if (BLOCKED_EXTENSIONS.includes(ext)) return "code";
1825
- if (BLOCKED_PATH_PREFIXES.some((p) => rel.startsWith(p))) return "code";
1826
- return "exempt";
1827
- }
1828
- async function hasRecentTomDispatch(now = Date.now()) {
1829
- try {
1830
- const client = getClient();
1831
- const result = await client.execute({
1832
- sql: `SELECT MAX(created_at) AS last_dispatch
1833
- FROM tasks
1834
- WHERE assigned_by = ? AND assigned_to = 'tom'`,
1835
- args: [GATED_AGENT]
1836
- });
1837
- const raw = result.rows[0]?.last_dispatch;
1838
- if (raw === null || raw === void 0) return false;
1839
- const parsed = Date.parse(String(raw));
1840
- if (Number.isNaN(parsed)) return false;
1841
- return now - parsed <= RECENT_DISPATCH_WINDOW_MS;
1842
- } catch {
1843
- return false;
1844
- }
1845
- }
1846
- function scratchpadPath(sessionId, homeDir = os2.homedir()) {
1847
- return path5.join(
1848
- homeDir,
1849
- ".exe-os",
1850
- "session-cache",
1851
- `yoshi-scratchpad-${sessionId}.txt`
1852
- );
1853
- }
1854
- function hasValidScratchpadEscape(sessionId, now = Date.now(), homeDir = os2.homedir()) {
1855
- const filePath = scratchpadPath(sessionId, homeDir);
1856
- if (!existsSync4(filePath)) return false;
1857
- try {
1858
- const stat = statSync(filePath);
1859
- if (now - stat.mtimeMs > SCRATCHPAD_MAX_AGE_MS) return false;
1860
- const body = readFileSync3(filePath, "utf-8");
1861
- return SCRATCHPAD_ESCAPE_PATTERN.test(body);
1862
- } catch {
1863
- return false;
1864
- }
1865
- }
1866
- async function evaluateDelegationGate(opts) {
1867
- if (opts.agentId !== GATED_AGENT) {
1868
- return { outcome: "allow", reason: "agent_not_gated" };
1869
- }
1870
- if (!/^(Write|Edit)$/.test(opts.toolName)) {
1871
- return { outcome: "allow", reason: "tool_not_gated" };
1872
- }
1873
- if (classifyPath(opts.filePath, opts.cwd) === "exempt") {
1874
- return { outcome: "allow", reason: "exempt_path" };
1875
- }
1876
- if (await hasRecentTomDispatch(opts.now)) {
1877
- return { outcome: "allow", reason: "recent_tom_dispatch" };
1878
- }
1879
- if (hasValidScratchpadEscape(opts.sessionId, opts.now, opts.homeDir)) {
1880
- return { outcome: "allow", reason: "scratchpad_escape" };
1881
- }
1882
- return { outcome: "block", reason: "no_delegation_no_escape" };
1883
- }
1884
- var GATED_AGENT, BLOCKED_PATH_PREFIXES, BLOCKED_FILENAMES, BLOCKED_DOCKERFILE_PREFIX, BLOCKED_EXTENSIONS, EXEMPT_MD_DIR_PREFIXES, RECENT_DISPATCH_WINDOW_MS, SCRATCHPAD_MAX_AGE_MS, SCRATCHPAD_ESCAPE_PATTERN, GATE_BLOCK_MESSAGE;
1885
- var init_yoshi_delegation_gate = __esm({
1886
- "src/lib/yoshi-delegation-gate.ts"() {
1887
- "use strict";
1888
- init_database();
1889
- GATED_AGENT = "yoshi";
1890
- BLOCKED_PATH_PREFIXES = [
1891
- "src/",
1892
- "tests/",
1893
- "scripts/",
1894
- "bin/",
1895
- ".github/workflows/",
1896
- ".claude/skills/"
1897
- ];
1898
- BLOCKED_FILENAMES = [
1899
- "package.json",
1900
- "tsconfig.json",
1901
- "docker-compose.yml"
1902
- ];
1903
- BLOCKED_DOCKERFILE_PREFIX = "Dockerfile";
1904
- BLOCKED_EXTENSIONS = [
1905
- ".ts",
1906
- ".js",
1907
- ".tsx",
1908
- ".jsx",
1909
- ".rs",
1910
- ".py",
1911
- ".go",
1912
- ".sh",
1913
- ".yaml",
1914
- ".yml",
1915
- ".prisma",
1916
- ".sql"
1917
- ];
1918
- EXEMPT_MD_DIR_PREFIXES = [
1919
- ".planning/",
1920
- "exe/output/",
1921
- "exe/yoshi/",
1922
- "exe/tom/",
1923
- "exe/",
1924
- "docs/"
1925
- ];
1926
- RECENT_DISPATCH_WINDOW_MS = 5 * 60 * 1e3;
1927
- SCRATCHPAD_MAX_AGE_MS = 10 * 60 * 1e3;
1928
- SCRATCHPAD_ESCAPE_PATTERN = /^TOM-CAPABLE:\s*NO\s*—\s*reason:\s*\S+/m;
1929
- GATE_BLOCK_MESSAGE = [
1930
- "\u26A0\uFE0F DELEGATE CHECK: this edit looks implementable by Tom.",
1931
- "Dispatch create_task now OR mark TOM-CAPABLE: NO with reason.",
1932
- "No code until decided."
1933
- ].join("\n");
1934
- }
1935
- });
1936
-
1937
2154
  // src/lib/review-gate.ts
1938
2155
  var review_gate_exports = {};
1939
2156
  __export(review_gate_exports, {
@@ -1943,12 +2160,12 @@ __export(review_gate_exports, {
1943
2160
  checkTestCoverage: () => checkTestCoverage,
1944
2161
  runReviewGate: () => runReviewGate
1945
2162
  });
1946
- import { execSync as execSync3 } from "child_process";
1947
- import { existsSync as existsSync5 } from "fs";
2163
+ import { execSync as execSync4 } from "child_process";
2164
+ import { existsSync as existsSync6 } from "fs";
1948
2165
  function checkCommitsExist(taskCreatedAt) {
1949
2166
  try {
1950
2167
  const since = new Date(taskCreatedAt).toISOString();
1951
- const output = execSync3(
2168
+ const output = execSync4(
1952
2169
  `git log --oneline --since="${since}" --no-merges 2>/dev/null`,
1953
2170
  { encoding: "utf8", timeout: 5e3 }
1954
2171
  ).trim();
@@ -1965,7 +2182,7 @@ function checkLayerBoundaries(taskCreatedAt) {
1965
2182
  const violations = [];
1966
2183
  try {
1967
2184
  const since = new Date(taskCreatedAt).toISOString();
1968
- const filesOutput = execSync3(
2185
+ const filesOutput = execSync4(
1969
2186
  `git log --since="${since}" --no-merges --name-only --pretty=format: 2>/dev/null`,
1970
2187
  { encoding: "utf8", timeout: 5e3 }
1971
2188
  ).trim();
@@ -1974,7 +2191,7 @@ function checkLayerBoundaries(taskCreatedAt) {
1974
2191
  const libFiles = changedFiles.filter((f) => f.startsWith("src/lib/"));
1975
2192
  for (const file of libFiles) {
1976
2193
  try {
1977
- const grepResult = execSync3(
2194
+ const grepResult = execSync4(
1978
2195
  `grep -nE "from ['"]\\.\\./(adapters|tui|runtime)/" "${file}" 2>/dev/null`,
1979
2196
  { encoding: "utf8", timeout: 3e3 }
1980
2197
  ).trim();
@@ -1994,7 +2211,7 @@ function checkTestCoverage(taskCreatedAt) {
1994
2211
  const warnings = [];
1995
2212
  try {
1996
2213
  const since = new Date(taskCreatedAt).toISOString();
1997
- const output = execSync3(
2214
+ const output = execSync4(
1998
2215
  `git log --since="${since}" --no-merges --diff-filter=A --name-only --pretty=format: 2>/dev/null`,
1999
2216
  { encoding: "utf8", timeout: 5e3 }
2000
2217
  ).trim();
@@ -2007,10 +2224,10 @@ function checkTestCoverage(taskCreatedAt) {
2007
2224
  const testPath = file.replace(/^src\//, "tests/").replace(/\.ts$/, ".test.ts");
2008
2225
  const baseName = file.split("/").pop()?.replace(/\.ts$/, "") ?? "";
2009
2226
  const testDir = testPath.substring(0, testPath.lastIndexOf("/"));
2010
- const hasDirectTest = existsSync5(testPath);
2227
+ const hasDirectTest = existsSync6(testPath);
2011
2228
  let hasRelatedTest = false;
2012
2229
  try {
2013
- const related = execSync3(
2230
+ const related = execSync4(
2014
2231
  `find "${testDir}" -name "*${baseName}*" -name "*.test.ts" 2>/dev/null`,
2015
2232
  { encoding: "utf8", timeout: 3e3 }
2016
2233
  ).trim();
@@ -2061,9 +2278,9 @@ var init_review_gate = __esm({
2061
2278
  });
2062
2279
 
2063
2280
  // src/adapters/claude/hooks/pre-tool-use.ts
2064
- import { existsSync as existsSync6, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
2065
- import { execSync as execSync4 } from "child_process";
2066
- import path6 from "path";
2281
+ import { existsSync as existsSync7, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
2282
+ import { execSync as execSync5 } from "child_process";
2283
+ import path7 from "path";
2067
2284
 
2068
2285
  // src/adapters/claude/active-agent.ts
2069
2286
  init_config();
@@ -2163,17 +2380,17 @@ if (!process.env.AGENT_ID) {
2163
2380
  var DELEGATION_TASK_THRESHOLD = 3;
2164
2381
  var CTO_ROLES = ["CTO", "executive"];
2165
2382
  var TOM_SESSION_PATTERN = /^tom.*-exe/;
2166
- var CACHE_DIR2 = path6.join(EXE_AI_DIR, "session-cache");
2383
+ var CACHE_DIR2 = path7.join(EXE_AI_DIR, "session-cache");
2167
2384
  var timeout = setTimeout(() => {
2168
2385
  process.exit(0);
2169
2386
  }, 5e3);
2170
2387
  timeout.unref();
2171
2388
  function getDelegationFlagPath() {
2172
- return path6.join(CACHE_DIR2, `delegation-checkpoint-${getSessionKey()}.json`);
2389
+ return path7.join(CACHE_DIR2, `delegation-checkpoint-${getSessionKey()}.json`);
2173
2390
  }
2174
2391
  function hasDelegationFired() {
2175
2392
  try {
2176
- return existsSync6(getDelegationFlagPath());
2393
+ return existsSync7(getDelegationFlagPath());
2177
2394
  } catch {
2178
2395
  return false;
2179
2396
  }
@@ -2187,7 +2404,7 @@ function markDelegationFired() {
2187
2404
  }
2188
2405
  function countTomSessions() {
2189
2406
  try {
2190
- const output = execSync4("tmux list-sessions 2>/dev/null", {
2407
+ const output = execSync5("tmux list-sessions 2>/dev/null", {
2191
2408
  encoding: "utf8",
2192
2409
  timeout: 2e3
2193
2410
  });
@@ -2222,7 +2439,8 @@ This write was prevented. Do NOT retry.`
2222
2439
  process.exit(0);
2223
2440
  }
2224
2441
  }
2225
- if (agent.agentId === "yoshi" && /^(Write|Edit)$/.test(data.tool_name)) {
2442
+ const { GATED_AGENT: gatedAgent } = await Promise.resolve().then(() => (init_yoshi_delegation_gate(), yoshi_delegation_gate_exports));
2443
+ if (agent.agentId === gatedAgent && /^(Write|Edit)$/.test(data.tool_name)) {
2226
2444
  const filePath = data.tool_input?.file_path ?? "";
2227
2445
  if (filePath) {
2228
2446
  try {
@@ -2284,7 +2502,8 @@ This write was prevented. Do NOT retry.`
2284
2502
  });
2285
2503
  if (taskRow.rows.length > 0) {
2286
2504
  const task = taskRow.rows[0];
2287
- if (task.assigned_to === "tom") {
2505
+ const { hasRole: hasEngineerRole } = await Promise.resolve().then(() => (init_employees(), employees_exports));
2506
+ if (hasEngineerRole(task.assigned_to, "Principal Engineer")) {
2288
2507
  const { runReviewGate: runReviewGate2 } = await Promise.resolve().then(() => (init_review_gate(), review_gate_exports));
2289
2508
  const gate = runReviewGate2({
2290
2509
  taskCreatedAt: task.created_at,