@askexenow/exe-os 0.8.33 → 0.8.37

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 (89) hide show
  1. package/dist/bin/backfill-conversations.js +341 -349
  2. package/dist/bin/backfill-responses.js +81 -13
  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 +1737 -1117
  6. package/dist/bin/exe-assign.js +89 -19
  7. package/dist/bin/exe-boot.js +951 -101
  8. package/dist/bin/exe-call.js +61 -2
  9. package/dist/bin/exe-dispatch.js +61 -13
  10. package/dist/bin/exe-doctor.js +63 -3
  11. package/dist/bin/exe-export-behaviors.js +71 -3
  12. package/dist/bin/exe-forget.js +69 -4
  13. package/dist/bin/exe-gateway.js +178 -45
  14. package/dist/bin/exe-heartbeat.js +79 -14
  15. package/dist/bin/exe-kill.js +71 -3
  16. package/dist/bin/exe-launch-agent.js +148 -14
  17. package/dist/bin/exe-link.js +1437 -0
  18. package/dist/bin/exe-new-employee.js +98 -13
  19. package/dist/bin/exe-pending-messages.js +74 -8
  20. package/dist/bin/exe-pending-notifications.js +63 -3
  21. package/dist/bin/exe-pending-reviews.js +77 -11
  22. package/dist/bin/exe-rename.js +1287 -0
  23. package/dist/bin/exe-review.js +73 -5
  24. package/dist/bin/exe-search.js +88 -14
  25. package/dist/bin/exe-session-cleanup.js +102 -28
  26. package/dist/bin/exe-status.js +64 -4
  27. package/dist/bin/exe-team.js +64 -4
  28. package/dist/bin/git-sweep.js +80 -5
  29. package/dist/bin/graph-backfill.js +71 -3
  30. package/dist/bin/graph-export.js +71 -3
  31. package/dist/bin/install.js +38 -8
  32. package/dist/bin/scan-tasks.js +80 -5
  33. package/dist/bin/setup.js +128 -10
  34. package/dist/bin/shard-migrate.js +71 -3
  35. package/dist/bin/wiki-sync.js +71 -3
  36. package/dist/gateway/index.js +179 -46
  37. package/dist/hooks/bug-report-worker.js +254 -28
  38. package/dist/hooks/commit-complete.js +80 -5
  39. package/dist/hooks/error-recall.js +89 -15
  40. package/dist/hooks/exe-heartbeat-hook.js +1 -1
  41. package/dist/hooks/ingest-worker.js +185 -51
  42. package/dist/hooks/ingest.js +1 -1
  43. package/dist/hooks/instructions-loaded.js +81 -6
  44. package/dist/hooks/notification.js +81 -6
  45. package/dist/hooks/post-compact.js +81 -6
  46. package/dist/hooks/pre-compact.js +81 -6
  47. package/dist/hooks/pre-tool-use.js +423 -196
  48. package/dist/hooks/prompt-ingest-worker.js +91 -23
  49. package/dist/hooks/prompt-submit.js +159 -45
  50. package/dist/hooks/response-ingest-worker.js +96 -23
  51. package/dist/hooks/session-end.js +81 -6
  52. package/dist/hooks/session-start.js +89 -15
  53. package/dist/hooks/stop.js +81 -6
  54. package/dist/hooks/subagent-stop.js +81 -6
  55. package/dist/hooks/summary-worker.js +807 -55
  56. package/dist/index.js +198 -60
  57. package/dist/lib/cloud-sync.js +703 -18
  58. package/dist/lib/consolidation.js +4 -4
  59. package/dist/lib/database.js +64 -2
  60. package/dist/lib/device-registry.js +70 -3
  61. package/dist/lib/employee-templates.js +26 -0
  62. package/dist/lib/employees.js +34 -1
  63. package/dist/lib/exe-daemon.js +207 -74
  64. package/dist/lib/hybrid-search.js +88 -14
  65. package/dist/lib/identity-templates.js +51 -0
  66. package/dist/lib/identity.js +3 -3
  67. package/dist/lib/messaging.js +65 -17
  68. package/dist/lib/reminders.js +3 -3
  69. package/dist/lib/schedules.js +63 -3
  70. package/dist/lib/skill-learning.js +3 -3
  71. package/dist/lib/status-brief.js +63 -5
  72. package/dist/lib/store.js +73 -4
  73. package/dist/lib/task-router.js +4 -2
  74. package/dist/lib/tasks.js +95 -28
  75. package/dist/lib/tmux-routing.js +92 -23
  76. package/dist/mcp/server.js +800 -74
  77. package/dist/mcp/tools/complete-reminder.js +3 -3
  78. package/dist/mcp/tools/create-reminder.js +3 -3
  79. package/dist/mcp/tools/create-task.js +198 -31
  80. package/dist/mcp/tools/deactivate-behavior.js +4 -4
  81. package/dist/mcp/tools/list-reminders.js +3 -3
  82. package/dist/mcp/tools/list-tasks.js +19 -9
  83. package/dist/mcp/tools/send-message.js +69 -21
  84. package/dist/mcp/tools/update-task.js +28 -18
  85. package/dist/runtime/index.js +166 -28
  86. package/dist/tui/App.js +193 -40
  87. package/package.json +7 -3
  88. package/src/commands/exe/afk.md +116 -0
  89. 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;
@@ -1422,7 +1793,8 @@ async function writeMemory(record) {
1422
1793
  has_error: record.has_error ? 1 : 0,
1423
1794
  raw_text: record.raw_text,
1424
1795
  vector: record.vector,
1425
- version: _nextVersion++,
1796
+ version: 0,
1797
+ // Placeholder — assigned atomically at flush time
1426
1798
  task_id: record.task_id ?? null,
1427
1799
  importance: record.importance ?? 5,
1428
1800
  status: record.status ?? "active",
@@ -1456,6 +1828,13 @@ async function flushBatch() {
1456
1828
  _flushing = true;
1457
1829
  try {
1458
1830
  const batch = _pendingRecords.slice(0);
1831
+ const client = getClient();
1832
+ const vResult = await client.execute("SELECT MAX(version) as max_v FROM memories");
1833
+ let baseVersion = (Number(vResult.rows[0]?.max_v) || 0) + 1;
1834
+ for (const row of batch) {
1835
+ row.version = baseVersion++;
1836
+ }
1837
+ _nextVersion = baseVersion;
1459
1838
  const buildStmt = (row) => {
1460
1839
  const hasVector = row.vector !== null;
1461
1840
  const taskId = row.task_id ?? null;
@@ -1780,160 +2159,6 @@ var init_store = __esm({
1780
2159
  }
1781
2160
  });
1782
2161
 
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
2162
  // src/lib/review-gate.ts
1938
2163
  var review_gate_exports = {};
1939
2164
  __export(review_gate_exports, {
@@ -1943,12 +2168,12 @@ __export(review_gate_exports, {
1943
2168
  checkTestCoverage: () => checkTestCoverage,
1944
2169
  runReviewGate: () => runReviewGate
1945
2170
  });
1946
- import { execSync as execSync3 } from "child_process";
1947
- import { existsSync as existsSync5 } from "fs";
2171
+ import { execSync as execSync4 } from "child_process";
2172
+ import { existsSync as existsSync6 } from "fs";
1948
2173
  function checkCommitsExist(taskCreatedAt) {
1949
2174
  try {
1950
2175
  const since = new Date(taskCreatedAt).toISOString();
1951
- const output = execSync3(
2176
+ const output = execSync4(
1952
2177
  `git log --oneline --since="${since}" --no-merges 2>/dev/null`,
1953
2178
  { encoding: "utf8", timeout: 5e3 }
1954
2179
  ).trim();
@@ -1965,7 +2190,7 @@ function checkLayerBoundaries(taskCreatedAt) {
1965
2190
  const violations = [];
1966
2191
  try {
1967
2192
  const since = new Date(taskCreatedAt).toISOString();
1968
- const filesOutput = execSync3(
2193
+ const filesOutput = execSync4(
1969
2194
  `git log --since="${since}" --no-merges --name-only --pretty=format: 2>/dev/null`,
1970
2195
  { encoding: "utf8", timeout: 5e3 }
1971
2196
  ).trim();
@@ -1974,7 +2199,7 @@ function checkLayerBoundaries(taskCreatedAt) {
1974
2199
  const libFiles = changedFiles.filter((f) => f.startsWith("src/lib/"));
1975
2200
  for (const file of libFiles) {
1976
2201
  try {
1977
- const grepResult = execSync3(
2202
+ const grepResult = execSync4(
1978
2203
  `grep -nE "from ['"]\\.\\./(adapters|tui|runtime)/" "${file}" 2>/dev/null`,
1979
2204
  { encoding: "utf8", timeout: 3e3 }
1980
2205
  ).trim();
@@ -1994,7 +2219,7 @@ function checkTestCoverage(taskCreatedAt) {
1994
2219
  const warnings = [];
1995
2220
  try {
1996
2221
  const since = new Date(taskCreatedAt).toISOString();
1997
- const output = execSync3(
2222
+ const output = execSync4(
1998
2223
  `git log --since="${since}" --no-merges --diff-filter=A --name-only --pretty=format: 2>/dev/null`,
1999
2224
  { encoding: "utf8", timeout: 5e3 }
2000
2225
  ).trim();
@@ -2007,10 +2232,10 @@ function checkTestCoverage(taskCreatedAt) {
2007
2232
  const testPath = file.replace(/^src\//, "tests/").replace(/\.ts$/, ".test.ts");
2008
2233
  const baseName = file.split("/").pop()?.replace(/\.ts$/, "") ?? "";
2009
2234
  const testDir = testPath.substring(0, testPath.lastIndexOf("/"));
2010
- const hasDirectTest = existsSync5(testPath);
2235
+ const hasDirectTest = existsSync6(testPath);
2011
2236
  let hasRelatedTest = false;
2012
2237
  try {
2013
- const related = execSync3(
2238
+ const related = execSync4(
2014
2239
  `find "${testDir}" -name "*${baseName}*" -name "*.test.ts" 2>/dev/null`,
2015
2240
  { encoding: "utf8", timeout: 3e3 }
2016
2241
  ).trim();
@@ -2061,9 +2286,9 @@ var init_review_gate = __esm({
2061
2286
  });
2062
2287
 
2063
2288
  // 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";
2289
+ import { existsSync as existsSync7, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
2290
+ import { execSync as execSync5 } from "child_process";
2291
+ import path7 from "path";
2067
2292
 
2068
2293
  // src/adapters/claude/active-agent.ts
2069
2294
  init_config();
@@ -2139,7 +2364,7 @@ function getActiveAgent() {
2139
2364
  "tmux display-message -p '#{session_name}' 2>/dev/null",
2140
2365
  { encoding: "utf8", timeout: 2e3 }
2141
2366
  ).trim();
2142
- const empMatch = sessionName.match(/^(\w+)-exe\d+$/);
2367
+ const empMatch = sessionName.match(/^([a-zA-Z]+)\d*-exe\d+$/);
2143
2368
  if (empMatch && empMatch[1] !== "exe") {
2144
2369
  return { agentId: empMatch[1], agentRole: "employee" };
2145
2370
  }
@@ -2163,17 +2388,17 @@ if (!process.env.AGENT_ID) {
2163
2388
  var DELEGATION_TASK_THRESHOLD = 3;
2164
2389
  var CTO_ROLES = ["CTO", "executive"];
2165
2390
  var TOM_SESSION_PATTERN = /^tom.*-exe/;
2166
- var CACHE_DIR2 = path6.join(EXE_AI_DIR, "session-cache");
2391
+ var CACHE_DIR2 = path7.join(EXE_AI_DIR, "session-cache");
2167
2392
  var timeout = setTimeout(() => {
2168
2393
  process.exit(0);
2169
2394
  }, 5e3);
2170
2395
  timeout.unref();
2171
2396
  function getDelegationFlagPath() {
2172
- return path6.join(CACHE_DIR2, `delegation-checkpoint-${getSessionKey()}.json`);
2397
+ return path7.join(CACHE_DIR2, `delegation-checkpoint-${getSessionKey()}.json`);
2173
2398
  }
2174
2399
  function hasDelegationFired() {
2175
2400
  try {
2176
- return existsSync6(getDelegationFlagPath());
2401
+ return existsSync7(getDelegationFlagPath());
2177
2402
  } catch {
2178
2403
  return false;
2179
2404
  }
@@ -2187,7 +2412,7 @@ function markDelegationFired() {
2187
2412
  }
2188
2413
  function countTomSessions() {
2189
2414
  try {
2190
- const output = execSync4("tmux list-sessions 2>/dev/null", {
2415
+ const output = execSync5("tmux list-sessions 2>/dev/null", {
2191
2416
  encoding: "utf8",
2192
2417
  timeout: 2e3
2193
2418
  });
@@ -2222,7 +2447,8 @@ This write was prevented. Do NOT retry.`
2222
2447
  process.exit(0);
2223
2448
  }
2224
2449
  }
2225
- if (agent.agentId === "yoshi" && /^(Write|Edit)$/.test(data.tool_name)) {
2450
+ const { GATED_AGENT: gatedAgent } = await Promise.resolve().then(() => (init_yoshi_delegation_gate(), yoshi_delegation_gate_exports));
2451
+ if (agent.agentId === gatedAgent && /^(Write|Edit)$/.test(data.tool_name)) {
2226
2452
  const filePath = data.tool_input?.file_path ?? "";
2227
2453
  if (filePath) {
2228
2454
  try {
@@ -2284,7 +2510,8 @@ This write was prevented. Do NOT retry.`
2284
2510
  });
2285
2511
  if (taskRow.rows.length > 0) {
2286
2512
  const task = taskRow.rows[0];
2287
- if (task.assigned_to === "tom") {
2513
+ const { hasRole: hasEngineerRole } = await Promise.resolve().then(() => (init_employees(), employees_exports));
2514
+ if (hasEngineerRole(task.assigned_to, "Principal Engineer")) {
2288
2515
  const { runReviewGate: runReviewGate2 } = await Promise.resolve().then(() => (init_review_gate(), review_gate_exports));
2289
2516
  const gate = runReviewGate2({
2290
2517
  taskCreatedAt: task.created_at,