@askexenow/exe-os 0.8.41 → 0.8.43
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/backfill-conversations.js +805 -642
- package/dist/bin/backfill-responses.js +804 -641
- package/dist/bin/backfill-vectors.js +791 -634
- package/dist/bin/cleanup-stale-review-tasks.js +788 -631
- package/dist/bin/cli.js +1345 -660
- package/dist/bin/exe-agent.js +20 -1
- package/dist/bin/exe-assign.js +1503 -1343
- package/dist/bin/exe-boot.js +2518 -1798
- package/dist/bin/exe-call.js +39 -1
- package/dist/bin/exe-cloud.js +15 -1
- package/dist/bin/exe-dispatch.js +39 -2
- package/dist/bin/exe-doctor.js +790 -633
- package/dist/bin/exe-export-behaviors.js +792 -637
- package/dist/bin/exe-forget.js +145 -0
- package/dist/bin/exe-gateway.js +2500 -1877
- package/dist/bin/exe-heartbeat.js +147 -1
- package/dist/bin/exe-kill.js +795 -640
- package/dist/bin/exe-launch-agent.js +2168 -2008
- package/dist/bin/exe-link.js +28 -2
- package/dist/bin/exe-new-employee.js +25 -3
- package/dist/bin/exe-pending-messages.js +146 -1
- package/dist/bin/exe-pending-notifications.js +788 -631
- package/dist/bin/exe-pending-reviews.js +147 -1
- package/dist/bin/exe-rename.js +23 -0
- package/dist/bin/exe-review.js +490 -327
- package/dist/bin/exe-search.js +154 -3
- package/dist/bin/exe-session-cleanup.js +2466 -413
- package/dist/bin/exe-status.js +474 -317
- package/dist/bin/exe-team.js +474 -317
- package/dist/bin/git-sweep.js +2690 -150
- package/dist/bin/graph-backfill.js +794 -637
- package/dist/bin/graph-export.js +798 -641
- package/dist/bin/scan-tasks.js +2951 -44
- package/dist/bin/setup.js +62 -26
- package/dist/bin/shard-migrate.js +792 -637
- package/dist/bin/wiki-sync.js +794 -637
- package/dist/gateway/index.js +2504 -1895
- package/dist/hooks/bug-report-worker.js +2118 -576
- package/dist/hooks/commit-complete.js +2689 -149
- package/dist/hooks/error-recall.js +154 -3
- package/dist/hooks/ingest-worker.js +1439 -815
- package/dist/hooks/instructions-loaded.js +151 -0
- package/dist/hooks/notification.js +153 -2
- package/dist/hooks/post-compact.js +164 -0
- package/dist/hooks/pre-compact.js +3073 -101
- package/dist/hooks/pre-tool-use.js +151 -0
- package/dist/hooks/prompt-ingest-worker.js +1714 -1537
- package/dist/hooks/prompt-submit.js +2658 -1113
- package/dist/hooks/response-ingest-worker.js +170 -6
- package/dist/hooks/session-end.js +153 -2
- package/dist/hooks/session-start.js +154 -3
- package/dist/hooks/stop.js +151 -0
- package/dist/hooks/subagent-stop.js +151 -0
- package/dist/hooks/summary-worker.js +179 -7
- package/dist/index.js +278 -100
- package/dist/lib/cloud-sync.js +28 -2
- package/dist/lib/consolidation.js +69 -2
- package/dist/lib/database.js +19 -0
- package/dist/lib/device-registry.js +19 -0
- package/dist/lib/employee-templates.js +20 -1
- package/dist/lib/exe-daemon.js +236 -16
- package/dist/lib/hybrid-search.js +154 -3
- package/dist/lib/license.js +15 -1
- package/dist/lib/messaging.js +39 -2
- package/dist/lib/schedules.js +792 -637
- package/dist/lib/store.js +796 -636
- package/dist/lib/tasks.js +1614 -1091
- package/dist/lib/tmux-routing.js +149 -9
- package/dist/mcp/server.js +1825 -1138
- package/dist/mcp/tools/create-task.js +2280 -828
- package/dist/mcp/tools/list-tasks.js +2788 -159
- package/dist/mcp/tools/send-message.js +39 -2
- package/dist/mcp/tools/update-task.js +64 -0
- package/dist/runtime/index.js +235 -67
- package/dist/tui/App.js +1452 -644
- package/package.json +3 -2
package/dist/lib/tasks.js
CHANGED
|
@@ -284,446 +284,68 @@ var init_notifications = __esm({
|
|
|
284
284
|
}
|
|
285
285
|
});
|
|
286
286
|
|
|
287
|
-
// src/lib/
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
import { mkdir as mkdir2, writeFile as writeFile2, appendFile } from "fs/promises";
|
|
292
|
-
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
293
|
-
async function writeCheckpoint(input) {
|
|
294
|
-
const client = getClient();
|
|
295
|
-
const row = await resolveTask(client, input.taskId);
|
|
296
|
-
const taskId = String(row.id);
|
|
297
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
298
|
-
const blockedByIds = [];
|
|
299
|
-
if (row.blocked_by) {
|
|
300
|
-
blockedByIds.push(String(row.blocked_by));
|
|
301
|
-
}
|
|
302
|
-
const checkpoint = {
|
|
303
|
-
step: input.step,
|
|
304
|
-
context_summary: input.contextSummary,
|
|
305
|
-
files_touched: input.filesTouched ?? [],
|
|
306
|
-
blocked_by_ids: blockedByIds,
|
|
307
|
-
last_checkpoint_at: now
|
|
308
|
-
};
|
|
309
|
-
const result = await client.execute({
|
|
310
|
-
sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
|
|
311
|
-
args: [JSON.stringify(checkpoint), now, taskId]
|
|
312
|
-
});
|
|
313
|
-
if (result.rowsAffected === 0) {
|
|
314
|
-
throw new Error(`Checkpoint write failed: task ${taskId} not found`);
|
|
315
|
-
}
|
|
316
|
-
const countResult = await client.execute({
|
|
317
|
-
sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
|
|
318
|
-
args: [taskId]
|
|
319
|
-
});
|
|
320
|
-
const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
|
|
321
|
-
return { checkpointCount };
|
|
322
|
-
}
|
|
323
|
-
function extractParentFromContext(contextBody) {
|
|
324
|
-
if (!contextBody) return null;
|
|
325
|
-
const match = contextBody.match(
|
|
326
|
-
/Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
|
|
327
|
-
);
|
|
328
|
-
return match ? match[1].toLowerCase() : null;
|
|
329
|
-
}
|
|
330
|
-
function slugify(title) {
|
|
331
|
-
return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
332
|
-
}
|
|
333
|
-
async function resolveTask(client, identifier) {
|
|
334
|
-
let result = await client.execute({
|
|
335
|
-
sql: "SELECT * FROM tasks WHERE id = ?",
|
|
336
|
-
args: [identifier]
|
|
337
|
-
});
|
|
338
|
-
if (result.rows.length === 1) return result.rows[0];
|
|
339
|
-
result = await client.execute({
|
|
340
|
-
sql: "SELECT * FROM tasks WHERE task_file LIKE ?",
|
|
341
|
-
args: [`%${identifier}%`]
|
|
342
|
-
});
|
|
343
|
-
if (result.rows.length === 1) return result.rows[0];
|
|
344
|
-
if (result.rows.length > 1) {
|
|
345
|
-
const exact = result.rows.filter(
|
|
346
|
-
(r) => String(r.task_file).endsWith(`/${identifier}.md`)
|
|
347
|
-
);
|
|
348
|
-
if (exact.length === 1) return exact[0];
|
|
349
|
-
const candidates = exact.length > 1 ? exact : result.rows;
|
|
350
|
-
const active = candidates.filter(
|
|
351
|
-
(r) => !["done", "cancelled"].includes(String(r.status))
|
|
352
|
-
);
|
|
353
|
-
if (active.length === 1) return active[0];
|
|
354
|
-
const matches = (active.length > 1 ? active : candidates).map((r) => `${String(r.task_file)} (${String(r.status)}, ${String(r.id)})`).join(", ");
|
|
355
|
-
throw new Error(
|
|
356
|
-
`Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
|
|
357
|
-
);
|
|
358
|
-
}
|
|
359
|
-
result = await client.execute({
|
|
360
|
-
sql: "SELECT * FROM tasks WHERE title LIKE ?",
|
|
361
|
-
args: [`%${identifier}%`]
|
|
362
|
-
});
|
|
363
|
-
if (result.rows.length === 1) return result.rows[0];
|
|
364
|
-
if (result.rows.length > 1) {
|
|
365
|
-
const active = result.rows.filter(
|
|
366
|
-
(r) => !["done", "cancelled"].includes(String(r.status))
|
|
367
|
-
);
|
|
368
|
-
if (active.length === 1) return active[0];
|
|
369
|
-
const matches = (active.length > 1 ? active : result.rows).map((r) => `"${String(r.title)}" (${String(r.status)}, ${String(r.id)})`).join(", ");
|
|
370
|
-
throw new Error(
|
|
371
|
-
`Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
|
|
372
|
-
);
|
|
373
|
-
}
|
|
374
|
-
throw new Error(`Task not found: ${identifier}`);
|
|
375
|
-
}
|
|
376
|
-
async function createTaskCore(input) {
|
|
377
|
-
const client = getClient();
|
|
378
|
-
const id = crypto2.randomUUID();
|
|
379
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
380
|
-
const slug = slugify(input.title);
|
|
381
|
-
const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
|
|
382
|
-
let blockedById = null;
|
|
383
|
-
const initialStatus = input.blockedBy ? "blocked" : "open";
|
|
384
|
-
if (input.blockedBy) {
|
|
385
|
-
const blocker = await resolveTask(client, input.blockedBy);
|
|
386
|
-
blockedById = String(blocker.id);
|
|
387
|
-
}
|
|
388
|
-
let parentTaskId = null;
|
|
389
|
-
let parentRef = input.parentTaskId;
|
|
390
|
-
if (!parentRef) {
|
|
391
|
-
const extracted = extractParentFromContext(input.context);
|
|
392
|
-
if (extracted) {
|
|
393
|
-
parentRef = extracted;
|
|
394
|
-
process.stderr.write(
|
|
395
|
-
"[create_task] auto-populated parent_task_id from context body \u2014 dispatchers should pass parent_task_id explicitly\n"
|
|
396
|
-
);
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
if (parentRef) {
|
|
400
|
-
try {
|
|
401
|
-
const parent = await resolveTask(client, parentRef);
|
|
402
|
-
parentTaskId = String(parent.id);
|
|
403
|
-
} catch (err) {
|
|
404
|
-
if (!input.parentTaskId) {
|
|
405
|
-
throw new Error(
|
|
406
|
-
`create_task: parent reference "${parentRef}" in context body does not resolve to an existing task`
|
|
407
|
-
);
|
|
408
|
-
}
|
|
409
|
-
throw err;
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
let warning;
|
|
413
|
-
const dupCheck = await client.execute({
|
|
414
|
-
sql: "SELECT id FROM tasks WHERE title = ? AND assigned_to = ? AND status IN ('open', 'in_progress', 'blocked')",
|
|
415
|
-
args: [input.title, input.assignedTo]
|
|
416
|
-
});
|
|
417
|
-
if (dupCheck.rows.length > 0) {
|
|
418
|
-
warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
|
|
419
|
-
}
|
|
420
|
-
if (input.baseDir) {
|
|
421
|
-
try {
|
|
422
|
-
await mkdir2(path3.join(input.baseDir, "exe", "output"), { recursive: true });
|
|
423
|
-
await mkdir2(path3.join(input.baseDir, "exe", "research"), { recursive: true });
|
|
424
|
-
await ensureArchitectureDoc(input.baseDir, input.projectName);
|
|
425
|
-
await ensureGitignoreExe(input.baseDir);
|
|
426
|
-
} catch {
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
const complexity = input.complexity ?? "standard";
|
|
430
|
-
await client.execute({
|
|
431
|
-
sql: `INSERT INTO tasks (id, title, assigned_to, assigned_by, project_name, priority, status, task_file, blocked_by, parent_task_id, reviewer, context, complexity, budget_tokens, budget_fallback_model, tokens_used, tokens_warned_at, created_at, updated_at)
|
|
432
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
433
|
-
args: [
|
|
434
|
-
id,
|
|
435
|
-
input.title,
|
|
436
|
-
input.assignedTo,
|
|
437
|
-
input.assignedBy,
|
|
438
|
-
input.projectName,
|
|
439
|
-
input.priority,
|
|
440
|
-
initialStatus,
|
|
441
|
-
taskFile,
|
|
442
|
-
blockedById,
|
|
443
|
-
parentTaskId,
|
|
444
|
-
input.reviewer ?? null,
|
|
445
|
-
input.context,
|
|
446
|
-
complexity,
|
|
447
|
-
input.budgetTokens ?? null,
|
|
448
|
-
input.budgetFallbackModel ?? null,
|
|
449
|
-
0,
|
|
450
|
-
null,
|
|
451
|
-
now,
|
|
452
|
-
now
|
|
453
|
-
]
|
|
454
|
-
});
|
|
455
|
-
return {
|
|
456
|
-
id,
|
|
457
|
-
title: input.title,
|
|
458
|
-
assignedTo: input.assignedTo,
|
|
459
|
-
assignedBy: input.assignedBy,
|
|
460
|
-
projectName: input.projectName,
|
|
461
|
-
priority: input.priority,
|
|
462
|
-
status: initialStatus,
|
|
463
|
-
taskFile,
|
|
464
|
-
createdAt: now,
|
|
465
|
-
updatedAt: now,
|
|
466
|
-
warning,
|
|
467
|
-
budgetTokens: input.budgetTokens ?? null,
|
|
468
|
-
budgetFallbackModel: input.budgetFallbackModel ?? null,
|
|
469
|
-
tokensUsed: 0,
|
|
470
|
-
tokensWarnedAt: null
|
|
471
|
-
};
|
|
472
|
-
}
|
|
473
|
-
async function listTasks(input) {
|
|
474
|
-
const client = getClient();
|
|
475
|
-
const conditions = [];
|
|
476
|
-
const args = [];
|
|
477
|
-
if (input.assignedTo) {
|
|
478
|
-
conditions.push("assigned_to = ?");
|
|
479
|
-
args.push(input.assignedTo);
|
|
480
|
-
}
|
|
481
|
-
if (input.status) {
|
|
482
|
-
conditions.push("status = ?");
|
|
483
|
-
args.push(input.status);
|
|
484
|
-
} else {
|
|
485
|
-
conditions.push("status IN ('open', 'in_progress', 'blocked')");
|
|
486
|
-
}
|
|
487
|
-
if (input.projectName) {
|
|
488
|
-
conditions.push("project_name = ?");
|
|
489
|
-
args.push(input.projectName);
|
|
490
|
-
}
|
|
491
|
-
if (input.priority) {
|
|
492
|
-
conditions.push("priority = ?");
|
|
493
|
-
args.push(input.priority);
|
|
494
|
-
}
|
|
495
|
-
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
496
|
-
const result = await client.execute({
|
|
497
|
-
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`,
|
|
498
|
-
args
|
|
499
|
-
});
|
|
500
|
-
return result.rows.map((r) => ({
|
|
501
|
-
id: String(r.id),
|
|
502
|
-
title: String(r.title),
|
|
503
|
-
assignedTo: String(r.assigned_to),
|
|
504
|
-
assignedBy: String(r.assigned_by),
|
|
505
|
-
projectName: String(r.project_name),
|
|
506
|
-
priority: String(r.priority),
|
|
507
|
-
status: String(r.status),
|
|
508
|
-
taskFile: String(r.task_file),
|
|
509
|
-
createdAt: String(r.created_at),
|
|
510
|
-
updatedAt: String(r.updated_at),
|
|
511
|
-
checkpointCount: Number(r.checkpoint_count ?? 0),
|
|
512
|
-
budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
|
|
513
|
-
budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
|
|
514
|
-
tokensUsed: Number(r.tokens_used ?? 0),
|
|
515
|
-
tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
|
|
516
|
-
}));
|
|
517
|
-
}
|
|
518
|
-
function checkStaleCompletion(taskContext, taskCreatedAt) {
|
|
519
|
-
if (!taskContext) return null;
|
|
520
|
-
if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
|
|
521
|
-
try {
|
|
522
|
-
const since = new Date(taskCreatedAt).toISOString();
|
|
523
|
-
const branch = execSync(
|
|
524
|
-
"git rev-parse --abbrev-ref HEAD 2>/dev/null",
|
|
525
|
-
{ encoding: "utf8", timeout: 3e3 }
|
|
526
|
-
).trim();
|
|
527
|
-
const branchArg = branch && branch !== "HEAD" ? branch : "";
|
|
528
|
-
const commitCount = execSync(
|
|
529
|
-
`git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
|
|
530
|
-
{ encoding: "utf8", timeout: 5e3 }
|
|
531
|
-
).trim();
|
|
532
|
-
const count = parseInt(commitCount, 10);
|
|
533
|
-
if (count === 0) {
|
|
534
|
-
return "WARNING: task closed with no new commits since creation. Verify work was actually produced.";
|
|
535
|
-
}
|
|
536
|
-
return null;
|
|
537
|
-
} catch {
|
|
538
|
-
return null;
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
async function updateTaskStatus(input) {
|
|
542
|
-
const client = getClient();
|
|
543
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
544
|
-
const row = await resolveTask(client, input.taskId);
|
|
545
|
-
const taskId = String(row.id);
|
|
546
|
-
const taskFile = String(row.task_file);
|
|
547
|
-
if (input.status === "done" && String(row.assigned_by) === "system" && taskFile.includes("review-")) {
|
|
548
|
-
process.stderr.write(
|
|
549
|
-
`[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
|
|
550
|
-
`
|
|
551
|
-
);
|
|
552
|
-
}
|
|
553
|
-
if (input.status === "done") {
|
|
554
|
-
const existingRow = await client.execute({
|
|
555
|
-
sql: "SELECT context, created_at FROM tasks WHERE id = ?",
|
|
556
|
-
args: [taskId]
|
|
557
|
-
});
|
|
558
|
-
if (existingRow.rows.length > 0) {
|
|
559
|
-
const ctx = existingRow.rows[0];
|
|
560
|
-
const warning = checkStaleCompletion(ctx.context, ctx.created_at);
|
|
561
|
-
if (warning) {
|
|
562
|
-
input.result = input.result ? `\u26A0\uFE0F ${warning}
|
|
563
|
-
|
|
564
|
-
${input.result}` : `\u26A0\uFE0F ${warning}`;
|
|
565
|
-
process.stderr.write(`[tasks] ${warning} (task: ${taskId})
|
|
566
|
-
`);
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
if (input.status === "in_progress") {
|
|
571
|
-
const tmuxSession = process.env.TMUX_PANE ?? process.env.MY_TMUX_SESSION ?? "unknown";
|
|
572
|
-
const claim = await client.execute({
|
|
573
|
-
sql: `UPDATE tasks
|
|
574
|
-
SET status = 'in_progress', assigned_tmux = ?, updated_at = ?
|
|
575
|
-
WHERE id = ? AND status = 'open'`,
|
|
576
|
-
args: [tmuxSession, now, taskId]
|
|
577
|
-
});
|
|
578
|
-
if (claim.rowsAffected === 0) {
|
|
579
|
-
const current = await client.execute({
|
|
580
|
-
sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
|
|
581
|
-
args: [taskId]
|
|
582
|
-
});
|
|
583
|
-
const cur = current.rows[0];
|
|
584
|
-
const status = cur?.status ?? "unknown";
|
|
585
|
-
const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
|
|
586
|
-
throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
|
|
587
|
-
}
|
|
588
|
-
try {
|
|
589
|
-
await writeCheckpoint({
|
|
590
|
-
taskId,
|
|
591
|
-
step: "claimed",
|
|
592
|
-
contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
|
|
593
|
-
});
|
|
594
|
-
} catch {
|
|
595
|
-
}
|
|
596
|
-
return { row, taskFile, now, taskId };
|
|
597
|
-
}
|
|
598
|
-
if (input.result) {
|
|
599
|
-
await client.execute({
|
|
600
|
-
sql: "UPDATE tasks SET status = ?, result = ?, updated_at = ? WHERE id = ?",
|
|
601
|
-
args: [input.status, input.result, now, taskId]
|
|
602
|
-
});
|
|
603
|
-
} else {
|
|
604
|
-
await client.execute({
|
|
605
|
-
sql: "UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?",
|
|
606
|
-
args: [input.status, now, taskId]
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
|
-
try {
|
|
610
|
-
await writeCheckpoint({
|
|
611
|
-
taskId,
|
|
612
|
-
step: `status_transition:${input.status}`,
|
|
613
|
-
contextSummary: input.result ? `Transitioned to ${input.status}. Result: ${input.result.slice(0, 500)}` : `Transitioned to ${input.status}.`
|
|
614
|
-
});
|
|
615
|
-
} catch {
|
|
616
|
-
}
|
|
617
|
-
return { row, taskFile, now, taskId };
|
|
618
|
-
}
|
|
619
|
-
async function deleteTaskCore(taskId, _baseDir) {
|
|
620
|
-
const client = getClient();
|
|
621
|
-
const row = await resolveTask(client, taskId);
|
|
622
|
-
const id = String(row.id);
|
|
623
|
-
const taskFile = String(row.task_file);
|
|
624
|
-
const assignedTo = String(row.assigned_to);
|
|
625
|
-
const assignedBy = String(row.assigned_by);
|
|
626
|
-
await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
|
|
627
|
-
const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
|
|
628
|
-
return { taskFile, assignedTo, assignedBy, taskSlug };
|
|
629
|
-
}
|
|
630
|
-
async function ensureArchitectureDoc(baseDir, projectName) {
|
|
631
|
-
const archPath = path3.join(baseDir, "exe", "ARCHITECTURE.md");
|
|
632
|
-
try {
|
|
633
|
-
if (existsSync3(archPath)) return;
|
|
634
|
-
const template = [
|
|
635
|
-
`# ${projectName} \u2014 System Architecture`,
|
|
636
|
-
"",
|
|
637
|
-
"> Employees: read this before every task. Update it when you change system structure.",
|
|
638
|
-
`> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
|
|
639
|
-
"",
|
|
640
|
-
"## Overview",
|
|
641
|
-
"",
|
|
642
|
-
"<!-- Describe what this system does, its main components, and how they connect. -->",
|
|
643
|
-
"",
|
|
644
|
-
"## Key Components",
|
|
645
|
-
"",
|
|
646
|
-
"<!-- List the major modules, services, or subsystems. -->",
|
|
647
|
-
"",
|
|
648
|
-
"## Data Flow",
|
|
649
|
-
"",
|
|
650
|
-
"<!-- How does data move through the system? What writes where? -->",
|
|
651
|
-
"",
|
|
652
|
-
"## Invariants",
|
|
653
|
-
"",
|
|
654
|
-
"<!-- Rules that must never be violated. What breaks if these are wrong? -->",
|
|
655
|
-
"",
|
|
656
|
-
"## Dependencies",
|
|
657
|
-
"",
|
|
658
|
-
"<!-- What depends on what? If I change X, what else is affected? -->",
|
|
659
|
-
""
|
|
660
|
-
].join("\n");
|
|
661
|
-
await writeFile2(archPath, template, "utf-8");
|
|
662
|
-
} catch {
|
|
663
|
-
}
|
|
664
|
-
}
|
|
665
|
-
async function ensureGitignoreExe(baseDir) {
|
|
666
|
-
const gitignorePath = path3.join(baseDir, ".gitignore");
|
|
667
|
-
try {
|
|
668
|
-
if (existsSync3(gitignorePath)) {
|
|
669
|
-
const content = readFileSync3(gitignorePath, "utf-8");
|
|
670
|
-
if (/^\/?exe\/?$/m.test(content)) return;
|
|
671
|
-
await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
|
|
672
|
-
} else {
|
|
673
|
-
await writeFile2(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
|
|
674
|
-
}
|
|
675
|
-
} catch {
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
|
|
679
|
-
var init_tasks_crud = __esm({
|
|
680
|
-
"src/lib/tasks-crud.ts"() {
|
|
681
|
-
"use strict";
|
|
682
|
-
init_database();
|
|
683
|
-
DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
|
|
684
|
-
TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
|
|
685
|
-
}
|
|
686
|
-
});
|
|
687
|
-
|
|
688
|
-
// src/lib/employees.ts
|
|
689
|
-
import { readFile as readFile2, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
|
|
690
|
-
import { existsSync as existsSync4, symlinkSync, readlinkSync, readFileSync as readFileSync4 } from "fs";
|
|
691
|
-
import { execSync as execSync2 } from "child_process";
|
|
692
|
-
import path4 from "path";
|
|
693
|
-
function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
|
|
694
|
-
if (!existsSync4(employeesPath)) return [];
|
|
695
|
-
try {
|
|
696
|
-
return JSON.parse(readFileSync4(employeesPath, "utf-8"));
|
|
697
|
-
} catch {
|
|
698
|
-
return [];
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
function getEmployee(employees, name) {
|
|
702
|
-
return employees.find((e) => e.name.toLowerCase() === name.toLowerCase());
|
|
703
|
-
}
|
|
704
|
-
function isMultiInstance(agentName, employees) {
|
|
705
|
-
const roster = employees ?? loadEmployeesSync();
|
|
706
|
-
const emp = getEmployee(roster, agentName);
|
|
707
|
-
if (!emp) return false;
|
|
708
|
-
return MULTI_INSTANCE_ROLES.has(emp.role.toLowerCase());
|
|
709
|
-
}
|
|
710
|
-
var EMPLOYEES_PATH, MULTI_INSTANCE_ROLES;
|
|
711
|
-
var init_employees = __esm({
|
|
712
|
-
"src/lib/employees.ts"() {
|
|
287
|
+
// src/lib/state-bus.ts
|
|
288
|
+
var StateBus, orgBus;
|
|
289
|
+
var init_state_bus = __esm({
|
|
290
|
+
"src/lib/state-bus.ts"() {
|
|
713
291
|
"use strict";
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
292
|
+
StateBus = class {
|
|
293
|
+
handlers = /* @__PURE__ */ new Map();
|
|
294
|
+
globalHandlers = /* @__PURE__ */ new Set();
|
|
295
|
+
/** Emit an event to all subscribers */
|
|
296
|
+
emit(event) {
|
|
297
|
+
const typeHandlers = this.handlers.get(event.type);
|
|
298
|
+
if (typeHandlers) {
|
|
299
|
+
for (const handler of typeHandlers) {
|
|
300
|
+
try {
|
|
301
|
+
handler(event);
|
|
302
|
+
} catch {
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
for (const handler of this.globalHandlers) {
|
|
307
|
+
try {
|
|
308
|
+
handler(event);
|
|
309
|
+
} catch {
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/** Subscribe to a specific event type */
|
|
314
|
+
on(type, handler) {
|
|
315
|
+
if (!this.handlers.has(type)) {
|
|
316
|
+
this.handlers.set(type, /* @__PURE__ */ new Set());
|
|
317
|
+
}
|
|
318
|
+
this.handlers.get(type).add(handler);
|
|
319
|
+
}
|
|
320
|
+
/** Subscribe to ALL events */
|
|
321
|
+
onAny(handler) {
|
|
322
|
+
this.globalHandlers.add(handler);
|
|
323
|
+
}
|
|
324
|
+
/** Unsubscribe from a specific event type */
|
|
325
|
+
off(type, handler) {
|
|
326
|
+
this.handlers.get(type)?.delete(handler);
|
|
327
|
+
}
|
|
328
|
+
/** Unsubscribe from ALL events */
|
|
329
|
+
offAny(handler) {
|
|
330
|
+
this.globalHandlers.delete(handler);
|
|
331
|
+
}
|
|
332
|
+
/** Remove all listeners */
|
|
333
|
+
clear() {
|
|
334
|
+
this.handlers.clear();
|
|
335
|
+
this.globalHandlers.clear();
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
orgBus = new StateBus();
|
|
717
339
|
}
|
|
718
340
|
});
|
|
719
341
|
|
|
720
342
|
// src/lib/session-registry.ts
|
|
721
|
-
import { readFileSync as
|
|
722
|
-
import
|
|
343
|
+
import { readFileSync as readFileSync3, writeFileSync, mkdirSync, existsSync as existsSync3 } from "fs";
|
|
344
|
+
import path3 from "path";
|
|
723
345
|
import os3 from "os";
|
|
724
346
|
function registerSession(entry) {
|
|
725
|
-
const dir =
|
|
726
|
-
if (!
|
|
347
|
+
const dir = path3.dirname(REGISTRY_PATH);
|
|
348
|
+
if (!existsSync3(dir)) {
|
|
727
349
|
mkdirSync(dir, { recursive: true });
|
|
728
350
|
}
|
|
729
351
|
const sessions = listSessions();
|
|
@@ -737,7 +359,7 @@ function registerSession(entry) {
|
|
|
737
359
|
}
|
|
738
360
|
function listSessions() {
|
|
739
361
|
try {
|
|
740
|
-
const raw =
|
|
362
|
+
const raw = readFileSync3(REGISTRY_PATH, "utf8");
|
|
741
363
|
return JSON.parse(raw);
|
|
742
364
|
} catch {
|
|
743
365
|
return [];
|
|
@@ -747,18 +369,18 @@ var REGISTRY_PATH;
|
|
|
747
369
|
var init_session_registry = __esm({
|
|
748
370
|
"src/lib/session-registry.ts"() {
|
|
749
371
|
"use strict";
|
|
750
|
-
REGISTRY_PATH =
|
|
372
|
+
REGISTRY_PATH = path3.join(os3.homedir(), ".exe-os", "session-registry.json");
|
|
751
373
|
}
|
|
752
374
|
});
|
|
753
375
|
|
|
754
376
|
// src/lib/session-key.ts
|
|
755
|
-
import { execSync
|
|
377
|
+
import { execSync } from "child_process";
|
|
756
378
|
function getSessionKey() {
|
|
757
379
|
if (_cached) return _cached;
|
|
758
380
|
let pid = process.ppid;
|
|
759
381
|
for (let i = 0; i < 10; i++) {
|
|
760
382
|
try {
|
|
761
|
-
const info =
|
|
383
|
+
const info = execSync(`ps -p ${pid} -o ppid=,comm=`, {
|
|
762
384
|
encoding: "utf8",
|
|
763
385
|
timeout: 2e3
|
|
764
386
|
}).trim();
|
|
@@ -877,782 +499,1654 @@ var init_tmux_transport = __esm({
|
|
|
877
499
|
}
|
|
878
500
|
});
|
|
879
501
|
|
|
880
|
-
// src/lib/transport.ts
|
|
881
|
-
function getTransport() {
|
|
882
|
-
if (!_transport) {
|
|
883
|
-
const { TmuxTransport: TmuxTransport2 } = (init_tmux_transport(), __toCommonJS(tmux_transport_exports));
|
|
884
|
-
_transport = new TmuxTransport2();
|
|
502
|
+
// src/lib/transport.ts
|
|
503
|
+
function getTransport() {
|
|
504
|
+
if (!_transport) {
|
|
505
|
+
const { TmuxTransport: TmuxTransport2 } = (init_tmux_transport(), __toCommonJS(tmux_transport_exports));
|
|
506
|
+
_transport = new TmuxTransport2();
|
|
507
|
+
}
|
|
508
|
+
return _transport;
|
|
509
|
+
}
|
|
510
|
+
var _transport;
|
|
511
|
+
var init_transport = __esm({
|
|
512
|
+
"src/lib/transport.ts"() {
|
|
513
|
+
"use strict";
|
|
514
|
+
_transport = null;
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// src/lib/cc-agent-support.ts
|
|
519
|
+
import { execSync as execSync2 } from "child_process";
|
|
520
|
+
function _resetCcAgentSupportCache() {
|
|
521
|
+
_cachedSupport = null;
|
|
522
|
+
}
|
|
523
|
+
function claudeSupportsAgentFlag() {
|
|
524
|
+
if (_cachedSupport !== null) return _cachedSupport;
|
|
525
|
+
try {
|
|
526
|
+
const helpOutput = execSync2("claude --help 2>&1", {
|
|
527
|
+
encoding: "utf-8",
|
|
528
|
+
timeout: 5e3
|
|
529
|
+
});
|
|
530
|
+
_cachedSupport = /(^|\s)--agent(\b|=)/.test(helpOutput);
|
|
531
|
+
} catch {
|
|
532
|
+
_cachedSupport = false;
|
|
533
|
+
}
|
|
534
|
+
return _cachedSupport;
|
|
535
|
+
}
|
|
536
|
+
var _cachedSupport;
|
|
537
|
+
var init_cc_agent_support = __esm({
|
|
538
|
+
"src/lib/cc-agent-support.ts"() {
|
|
539
|
+
"use strict";
|
|
540
|
+
_cachedSupport = null;
|
|
541
|
+
}
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// src/lib/mcp-prefix.ts
|
|
545
|
+
function expandDualPrefixTools(shortNames) {
|
|
546
|
+
const out = [];
|
|
547
|
+
for (const name of shortNames) {
|
|
548
|
+
for (const prefix of MCP_TOOL_PREFIXES) {
|
|
549
|
+
out.push(prefix + name);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return out;
|
|
553
|
+
}
|
|
554
|
+
var MCP_PRIMARY_KEY, MCP_LEGACY_KEY, MCP_TOOL_PREFIXES;
|
|
555
|
+
var init_mcp_prefix = __esm({
|
|
556
|
+
"src/lib/mcp-prefix.ts"() {
|
|
557
|
+
"use strict";
|
|
558
|
+
MCP_PRIMARY_KEY = "exe-os";
|
|
559
|
+
MCP_LEGACY_KEY = "exe-mem";
|
|
560
|
+
MCP_TOOL_PREFIXES = [
|
|
561
|
+
`mcp__${MCP_PRIMARY_KEY}__`,
|
|
562
|
+
`mcp__${MCP_LEGACY_KEY}__`
|
|
563
|
+
];
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
// src/lib/provider-table.ts
|
|
568
|
+
function detectActiveProvider(env = process.env) {
|
|
569
|
+
const baseUrl = env.ANTHROPIC_BASE_URL;
|
|
570
|
+
if (!baseUrl) return DEFAULT_PROVIDER;
|
|
571
|
+
for (const [name, cfg] of Object.entries(PROVIDER_TABLE)) {
|
|
572
|
+
if (cfg.baseUrl === baseUrl) return name;
|
|
573
|
+
}
|
|
574
|
+
return DEFAULT_PROVIDER;
|
|
575
|
+
}
|
|
576
|
+
var PROVIDER_TABLE, DEFAULT_PROVIDER;
|
|
577
|
+
var init_provider_table = __esm({
|
|
578
|
+
"src/lib/provider-table.ts"() {
|
|
579
|
+
"use strict";
|
|
580
|
+
PROVIDER_TABLE = {
|
|
581
|
+
opencode: {
|
|
582
|
+
baseUrl: "https://opencode.ai/zen/go",
|
|
583
|
+
apiKeyEnv: "OPENCODE_API_KEY",
|
|
584
|
+
defaultModel: "minimax-m2.7"
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
DEFAULT_PROVIDER = "default";
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
// src/lib/intercom-queue.ts
|
|
592
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, renameSync as renameSync2, existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
|
|
593
|
+
import path4 from "path";
|
|
594
|
+
import os4 from "os";
|
|
595
|
+
function ensureDir() {
|
|
596
|
+
const dir = path4.dirname(QUEUE_PATH);
|
|
597
|
+
if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
|
|
598
|
+
}
|
|
599
|
+
function readQueue() {
|
|
600
|
+
try {
|
|
601
|
+
if (!existsSync4(QUEUE_PATH)) return [];
|
|
602
|
+
return JSON.parse(readFileSync4(QUEUE_PATH, "utf8"));
|
|
603
|
+
} catch {
|
|
604
|
+
return [];
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
function writeQueue(queue) {
|
|
608
|
+
ensureDir();
|
|
609
|
+
const tmp = `${QUEUE_PATH}.tmp`;
|
|
610
|
+
writeFileSync2(tmp, JSON.stringify(queue, null, 2));
|
|
611
|
+
renameSync2(tmp, QUEUE_PATH);
|
|
612
|
+
}
|
|
613
|
+
function queueIntercom(targetSession, reason) {
|
|
614
|
+
const queue = readQueue();
|
|
615
|
+
const existing = queue.find((q) => q.targetSession === targetSession);
|
|
616
|
+
if (existing) {
|
|
617
|
+
existing.attempts++;
|
|
618
|
+
existing.queuedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
619
|
+
existing.reason = reason;
|
|
620
|
+
} else {
|
|
621
|
+
queue.push({
|
|
622
|
+
targetSession,
|
|
623
|
+
queuedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
624
|
+
attempts: 0,
|
|
625
|
+
reason
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
writeQueue(queue);
|
|
629
|
+
}
|
|
630
|
+
var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
|
|
631
|
+
var init_intercom_queue = __esm({
|
|
632
|
+
"src/lib/intercom-queue.ts"() {
|
|
633
|
+
"use strict";
|
|
634
|
+
QUEUE_PATH = path4.join(os4.homedir(), ".exe-os", "intercom-queue.json");
|
|
635
|
+
TTL_MS = 60 * 60 * 1e3;
|
|
636
|
+
INTERCOM_LOG = path4.join(os4.homedir(), ".exe-os", "intercom.log");
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
// src/lib/employees.ts
|
|
641
|
+
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
642
|
+
import { existsSync as existsSync5, symlinkSync, readlinkSync, readFileSync as readFileSync5 } from "fs";
|
|
643
|
+
import { execSync as execSync3 } from "child_process";
|
|
644
|
+
import path5 from "path";
|
|
645
|
+
function loadEmployeesSync(employeesPath = EMPLOYEES_PATH) {
|
|
646
|
+
if (!existsSync5(employeesPath)) return [];
|
|
647
|
+
try {
|
|
648
|
+
return JSON.parse(readFileSync5(employeesPath, "utf-8"));
|
|
649
|
+
} catch {
|
|
650
|
+
return [];
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
function getEmployee(employees, name) {
|
|
654
|
+
return employees.find((e) => e.name.toLowerCase() === name.toLowerCase());
|
|
655
|
+
}
|
|
656
|
+
function isMultiInstance(agentName, employees) {
|
|
657
|
+
const roster = employees ?? loadEmployeesSync();
|
|
658
|
+
const emp = getEmployee(roster, agentName);
|
|
659
|
+
if (!emp) return false;
|
|
660
|
+
return MULTI_INSTANCE_ROLES.has(emp.role.toLowerCase());
|
|
661
|
+
}
|
|
662
|
+
var EMPLOYEES_PATH, MULTI_INSTANCE_ROLES;
|
|
663
|
+
var init_employees = __esm({
|
|
664
|
+
"src/lib/employees.ts"() {
|
|
665
|
+
"use strict";
|
|
666
|
+
init_config();
|
|
667
|
+
EMPLOYEES_PATH = path5.join(EXE_AI_DIR, "exe-employees.json");
|
|
668
|
+
MULTI_INSTANCE_ROLES = /* @__PURE__ */ new Set(["principal engineer", "content production specialist", "staff code reviewer"]);
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
// src/lib/license.ts
|
|
673
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync3, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
|
|
674
|
+
import { randomUUID } from "crypto";
|
|
675
|
+
import path6 from "path";
|
|
676
|
+
import { jwtVerify, importSPKI } from "jose";
|
|
677
|
+
var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
|
|
678
|
+
var init_license = __esm({
|
|
679
|
+
"src/lib/license.ts"() {
|
|
680
|
+
"use strict";
|
|
681
|
+
init_config();
|
|
682
|
+
LICENSE_PATH = path6.join(EXE_AI_DIR, "license.key");
|
|
683
|
+
CACHE_PATH = path6.join(EXE_AI_DIR, "license-cache.json");
|
|
684
|
+
DEVICE_ID_PATH = path6.join(EXE_AI_DIR, "device-id");
|
|
685
|
+
PLAN_LIMITS = {
|
|
686
|
+
free: { devices: 1, employees: 1, memories: 5e3 },
|
|
687
|
+
pro: { devices: 2, employees: 5, memories: 1e5 },
|
|
688
|
+
team: { devices: 10, employees: 20, memories: 1e6 },
|
|
689
|
+
agency: { devices: 50, employees: 100, memories: 1e7 },
|
|
690
|
+
enterprise: { devices: -1, employees: -1, memories: -1 }
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
// src/lib/plan-limits.ts
|
|
696
|
+
import { readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
|
|
697
|
+
import path7 from "path";
|
|
698
|
+
function getLicenseSync() {
|
|
699
|
+
try {
|
|
700
|
+
if (!existsSync7(CACHE_PATH2)) return freeLicense();
|
|
701
|
+
const raw = JSON.parse(readFileSync7(CACHE_PATH2, "utf8"));
|
|
702
|
+
if (!raw.token || typeof raw.token !== "string") return freeLicense();
|
|
703
|
+
const parts = raw.token.split(".");
|
|
704
|
+
if (parts.length !== 3) return freeLicense();
|
|
705
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
|
|
706
|
+
const plan = payload.plan ?? "free";
|
|
707
|
+
const limits = PLAN_LIMITS[plan] ?? PLAN_LIMITS.free;
|
|
708
|
+
return {
|
|
709
|
+
valid: true,
|
|
710
|
+
plan,
|
|
711
|
+
email: payload.sub ?? "",
|
|
712
|
+
expiresAt: payload.exp ? new Date(payload.exp * 1e3).toISOString() : null,
|
|
713
|
+
deviceLimit: limits.devices,
|
|
714
|
+
employeeLimit: limits.employees,
|
|
715
|
+
memoryLimit: limits.memories
|
|
716
|
+
};
|
|
717
|
+
} catch {
|
|
718
|
+
return freeLicense();
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
function freeLicense() {
|
|
722
|
+
const limits = PLAN_LIMITS.free;
|
|
723
|
+
return {
|
|
724
|
+
valid: true,
|
|
725
|
+
plan: "free",
|
|
726
|
+
email: "",
|
|
727
|
+
expiresAt: null,
|
|
728
|
+
deviceLimit: limits.devices,
|
|
729
|
+
employeeLimit: limits.employees,
|
|
730
|
+
memoryLimit: limits.memories
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
function assertEmployeeLimitSync(rosterPath) {
|
|
734
|
+
const license = getLicenseSync();
|
|
735
|
+
if (license.employeeLimit < 0) return;
|
|
736
|
+
const filePath = rosterPath ?? EMPLOYEES_PATH;
|
|
737
|
+
let count = 0;
|
|
738
|
+
try {
|
|
739
|
+
if (existsSync7(filePath)) {
|
|
740
|
+
const raw = readFileSync7(filePath, "utf8");
|
|
741
|
+
const employees = JSON.parse(raw);
|
|
742
|
+
count = Array.isArray(employees) ? employees.length : 0;
|
|
743
|
+
}
|
|
744
|
+
} catch {
|
|
745
|
+
throw new PlanLimitError(
|
|
746
|
+
`Cannot verify employee count: roster unreadable at ${filePath}. Refusing to proceed. Check file permissions or upgrade plan.`
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
if (count >= license.employeeLimit) {
|
|
750
|
+
throw new PlanLimitError(
|
|
751
|
+
`Employee limit reached: ${count}/${license.employeeLimit} employees on the ${license.plan} plan. Upgrade at https://askexe.com to add more.`
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
var PlanLimitError, CACHE_PATH2;
|
|
756
|
+
var init_plan_limits = __esm({
|
|
757
|
+
"src/lib/plan-limits.ts"() {
|
|
758
|
+
"use strict";
|
|
759
|
+
init_database();
|
|
760
|
+
init_employees();
|
|
761
|
+
init_license();
|
|
762
|
+
init_config();
|
|
763
|
+
PlanLimitError = class extends Error {
|
|
764
|
+
constructor(message) {
|
|
765
|
+
super(message);
|
|
766
|
+
this.name = "PlanLimitError";
|
|
767
|
+
}
|
|
768
|
+
};
|
|
769
|
+
CACHE_PATH2 = path7.join(EXE_AI_DIR, "license-cache.json");
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
// src/lib/session-kill-telemetry.ts
|
|
774
|
+
import crypto2 from "crypto";
|
|
775
|
+
async function recordSessionKill(input) {
|
|
776
|
+
try {
|
|
777
|
+
const client = getClient();
|
|
778
|
+
await client.execute({
|
|
779
|
+
sql: `INSERT INTO session_kills
|
|
780
|
+
(id, session_name, agent_id, killed_at, reason,
|
|
781
|
+
ticks_idle, estimated_tokens_saved)
|
|
782
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
783
|
+
args: [
|
|
784
|
+
crypto2.randomUUID(),
|
|
785
|
+
input.sessionName,
|
|
786
|
+
input.agentId,
|
|
787
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
788
|
+
input.reason,
|
|
789
|
+
input.ticksIdle ?? null,
|
|
790
|
+
input.estimatedTokensSaved ?? null
|
|
791
|
+
]
|
|
792
|
+
});
|
|
793
|
+
} catch (err) {
|
|
794
|
+
process.stderr.write(
|
|
795
|
+
`[session-kill-telemetry] write failed: ${err instanceof Error ? err.message : String(err)}
|
|
796
|
+
`
|
|
797
|
+
);
|
|
885
798
|
}
|
|
886
|
-
return _transport;
|
|
887
799
|
}
|
|
888
|
-
var
|
|
889
|
-
|
|
890
|
-
"src/lib/transport.ts"() {
|
|
800
|
+
var init_session_kill_telemetry = __esm({
|
|
801
|
+
"src/lib/session-kill-telemetry.ts"() {
|
|
891
802
|
"use strict";
|
|
892
|
-
|
|
803
|
+
init_database();
|
|
893
804
|
}
|
|
894
805
|
});
|
|
895
806
|
|
|
896
|
-
// src/lib/
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
807
|
+
// src/lib/capacity-monitor.ts
|
|
808
|
+
var capacity_monitor_exports = {};
|
|
809
|
+
__export(capacity_monitor_exports, {
|
|
810
|
+
CTX_FLOOR_PERCENT: () => CTX_FLOOR_PERCENT,
|
|
811
|
+
_resetLastRelaunchCache: () => _resetLastRelaunchCache,
|
|
812
|
+
_resetPendingCapacityKills: () => _resetPendingCapacityKills,
|
|
813
|
+
confirmCapacityKill: () => confirmCapacityKill,
|
|
814
|
+
createOrRefreshResumeTask: () => createOrRefreshResumeTask,
|
|
815
|
+
extractContextPercent: () => extractContextPercent,
|
|
816
|
+
isAtCapacity: () => isAtCapacity,
|
|
817
|
+
isWithinRelaunchCooldown: () => isWithinRelaunchCooldown,
|
|
818
|
+
pollCapacityDead: () => pollCapacityDead
|
|
819
|
+
});
|
|
820
|
+
function resumeTaskTitle(agentId) {
|
|
821
|
+
return `${RESUME_TITLE_PREFIX} ${agentId} hit context capacity \u2014 continue open tasks`;
|
|
822
|
+
}
|
|
823
|
+
function buildResumeContext(agentId, openTasks) {
|
|
824
|
+
const taskList = openTasks.map(
|
|
825
|
+
(r, i) => `${i + 1}. [${String(r.priority).toUpperCase()}] ${String(r.title)} (${String(r.task_file)})`
|
|
826
|
+
).join("\n");
|
|
827
|
+
return [
|
|
828
|
+
"## Context",
|
|
829
|
+
"",
|
|
830
|
+
`${agentId} hit context capacity and was auto-relaunched by the capacity monitor.`,
|
|
831
|
+
"Call recall_my_memory first \u2014 search for 'CONTEXT CHECKPOINT'. Pick up where the previous session stopped.",
|
|
832
|
+
"",
|
|
833
|
+
`You have ${openTasks.length} open task(s). Work through them in priority order:`,
|
|
834
|
+
"",
|
|
835
|
+
taskList,
|
|
836
|
+
"",
|
|
837
|
+
"Read each task file and chain through them. Build and commit after each one."
|
|
838
|
+
].join("\n");
|
|
839
|
+
}
|
|
840
|
+
function filterPaneContent(paneOutput) {
|
|
841
|
+
return paneOutput.split("\n").filter((line) => {
|
|
842
|
+
if (CONTENT_LINE_PREFIX.test(line)) return false;
|
|
843
|
+
for (const marker of CONTENT_LINE_MARKERS) {
|
|
844
|
+
if (line.includes(marker)) return false;
|
|
845
|
+
}
|
|
846
|
+
for (const re of SOURCE_CODE_MARKERS) {
|
|
847
|
+
if (re.test(line)) return false;
|
|
848
|
+
}
|
|
849
|
+
return true;
|
|
850
|
+
}).join("\n");
|
|
851
|
+
}
|
|
852
|
+
function extractContextPercent(paneOutput) {
|
|
853
|
+
const match = paneOutput.match(CC_CONTEXT_BAR_RE);
|
|
854
|
+
if (!match) return null;
|
|
855
|
+
const parsed = Number.parseInt(match[2], 10);
|
|
856
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
857
|
+
}
|
|
858
|
+
function isAtCapacity(paneOutput) {
|
|
859
|
+
const filtered = filterPaneContent(paneOutput);
|
|
860
|
+
return CAPACITY_PATTERNS.some((p) => p.test(filtered));
|
|
861
|
+
}
|
|
862
|
+
function confirmCapacityKill(agentId, now = Date.now()) {
|
|
863
|
+
const pendingSince = _pendingCapacityKill.get(agentId);
|
|
864
|
+
if (pendingSince === void 0) {
|
|
865
|
+
_pendingCapacityKill.set(agentId, now);
|
|
866
|
+
return false;
|
|
867
|
+
}
|
|
868
|
+
if (now - pendingSince > CONFIRMATION_WINDOW_MS) {
|
|
869
|
+
_pendingCapacityKill.set(agentId, now);
|
|
870
|
+
return false;
|
|
871
|
+
}
|
|
872
|
+
_pendingCapacityKill.delete(agentId);
|
|
873
|
+
return true;
|
|
900
874
|
}
|
|
901
|
-
function
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
875
|
+
function _resetPendingCapacityKills() {
|
|
876
|
+
_pendingCapacityKill.clear();
|
|
877
|
+
}
|
|
878
|
+
function _resetLastRelaunchCache() {
|
|
879
|
+
_lastRelaunch.clear();
|
|
880
|
+
}
|
|
881
|
+
async function lastResumeCreatedAtMs(agentId) {
|
|
882
|
+
const client = getClient();
|
|
883
|
+
const result = await client.execute({
|
|
884
|
+
sql: `SELECT MAX(created_at) AS last_created_at
|
|
885
|
+
FROM tasks
|
|
886
|
+
WHERE assigned_to = ? AND title LIKE ?`,
|
|
887
|
+
args: [agentId, `${RESUME_TITLE_PREFIX} %`]
|
|
888
|
+
});
|
|
889
|
+
const raw = result.rows[0]?.last_created_at;
|
|
890
|
+
if (raw === null || raw === void 0) return null;
|
|
891
|
+
const parsed = Date.parse(String(raw));
|
|
892
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
893
|
+
}
|
|
894
|
+
async function isWithinRelaunchCooldown(agentId, now = Date.now()) {
|
|
895
|
+
const cached = _lastRelaunch.get(agentId);
|
|
896
|
+
if (cached !== void 0) return now - cached < RELAUNCH_COOLDOWN_MS;
|
|
897
|
+
const persisted = await lastResumeCreatedAtMs(agentId);
|
|
898
|
+
if (persisted === null) return false;
|
|
899
|
+
if (now - persisted >= RELAUNCH_COOLDOWN_MS) return false;
|
|
900
|
+
_lastRelaunch.set(agentId, persisted);
|
|
901
|
+
return true;
|
|
902
|
+
}
|
|
903
|
+
async function createOrRefreshResumeTask(agentId, projectDir, openTasks) {
|
|
904
|
+
const client = getClient();
|
|
905
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
906
|
+
const context = buildResumeContext(agentId, openTasks);
|
|
907
|
+
const existing = await client.execute({
|
|
908
|
+
sql: `SELECT id FROM tasks
|
|
909
|
+
WHERE assigned_to = ?
|
|
910
|
+
AND title LIKE ?
|
|
911
|
+
AND status IN (${RESUME_ACTIVE_STATUSES.map(() => "?").join(", ")})
|
|
912
|
+
ORDER BY created_at DESC
|
|
913
|
+
LIMIT 1`,
|
|
914
|
+
args: [agentId, RESUME_TITLE_LIKE_PATTERN, ...RESUME_ACTIVE_STATUSES]
|
|
915
|
+
});
|
|
916
|
+
if (existing.rows.length > 0) {
|
|
917
|
+
const taskId = String(existing.rows[0].id);
|
|
918
|
+
await client.execute({
|
|
919
|
+
sql: `UPDATE tasks SET context = ?, updated_at = ? WHERE id = ?`,
|
|
920
|
+
args: [context, now, taskId]
|
|
907
921
|
});
|
|
908
|
-
|
|
922
|
+
return { created: false, taskId };
|
|
923
|
+
}
|
|
924
|
+
const { createTask: createTask2 } = await Promise.resolve().then(() => (init_tasks(), tasks_exports));
|
|
925
|
+
const task = await createTask2({
|
|
926
|
+
title: resumeTaskTitle(agentId),
|
|
927
|
+
assignedTo: agentId,
|
|
928
|
+
assignedBy: "system",
|
|
929
|
+
projectName: projectDir.split("/").pop() ?? "unknown",
|
|
930
|
+
priority: "p0",
|
|
931
|
+
context,
|
|
932
|
+
baseDir: projectDir
|
|
933
|
+
});
|
|
934
|
+
return { created: true, taskId: task.id };
|
|
935
|
+
}
|
|
936
|
+
async function pollCapacityDead() {
|
|
937
|
+
const transport = getTransport();
|
|
938
|
+
const relaunched = [];
|
|
939
|
+
const registered = listSessions().filter(
|
|
940
|
+
(s) => s.agentId !== "exe"
|
|
941
|
+
);
|
|
942
|
+
if (registered.length === 0) return [];
|
|
943
|
+
let liveSessions;
|
|
944
|
+
try {
|
|
945
|
+
liveSessions = transport.listSessions();
|
|
909
946
|
} catch {
|
|
910
|
-
|
|
947
|
+
return [];
|
|
911
948
|
}
|
|
912
|
-
|
|
949
|
+
for (const entry of registered) {
|
|
950
|
+
const { windowName, agentId, projectDir } = entry;
|
|
951
|
+
if (!liveSessions.includes(windowName)) continue;
|
|
952
|
+
if (await isWithinRelaunchCooldown(agentId)) continue;
|
|
953
|
+
let pane;
|
|
954
|
+
try {
|
|
955
|
+
pane = transport.capturePane(windowName, 15);
|
|
956
|
+
} catch {
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
if (!isAtCapacity(pane)) continue;
|
|
960
|
+
const ctxPct = extractContextPercent(pane);
|
|
961
|
+
if (ctxPct !== null && ctxPct < CTX_FLOOR_PERCENT) {
|
|
962
|
+
process.stderr.write(
|
|
963
|
+
`[capacity-monitor] ctx-floor: ${agentId} at ${ctxPct}% in ${windowName} \u2014 below ${CTX_FLOOR_PERCENT}%. Skipping capacity kill (likely self-referential content or false positive).
|
|
964
|
+
`
|
|
965
|
+
);
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
if (!confirmCapacityKill(agentId)) {
|
|
969
|
+
process.stderr.write(
|
|
970
|
+
`[capacity-monitor] ${agentId} matched capacity pattern once in ${windowName}. Awaiting confirmation on next tick.
|
|
971
|
+
`
|
|
972
|
+
);
|
|
973
|
+
continue;
|
|
974
|
+
}
|
|
975
|
+
const verify = await verifyPaneAtCapacity(windowName);
|
|
976
|
+
if (!verify.atCapacity) {
|
|
977
|
+
process.stderr.write(
|
|
978
|
+
`[capacity-monitor] verifyPaneAtCapacity rejected kill for ${agentId} in ${windowName} (reason: ${verify.reason}). Skipping.
|
|
979
|
+
`
|
|
980
|
+
);
|
|
981
|
+
void recordSessionKill({
|
|
982
|
+
sessionName: windowName,
|
|
983
|
+
agentId,
|
|
984
|
+
reason: "capacity_false_positive_blocked"
|
|
985
|
+
});
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
988
|
+
process.stderr.write(
|
|
989
|
+
`[capacity-monitor] Detected ${agentId} at capacity in session ${windowName} (confirmed). Auto-relaunching.
|
|
990
|
+
`
|
|
991
|
+
);
|
|
992
|
+
try {
|
|
993
|
+
transport.kill(windowName);
|
|
994
|
+
void recordSessionKill({
|
|
995
|
+
sessionName: windowName,
|
|
996
|
+
agentId,
|
|
997
|
+
reason: "capacity"
|
|
998
|
+
});
|
|
999
|
+
const client = getClient();
|
|
1000
|
+
const openTasks = await client.execute({
|
|
1001
|
+
sql: `SELECT id, title, priority, task_file, status
|
|
1002
|
+
FROM tasks
|
|
1003
|
+
WHERE assigned_to = ? AND status IN ('open', 'in_progress')
|
|
1004
|
+
ORDER BY
|
|
1005
|
+
CASE priority WHEN 'p0' THEN 0 WHEN 'p1' THEN 1 WHEN 'p2' THEN 2 ELSE 3 END,
|
|
1006
|
+
created_at ASC
|
|
1007
|
+
LIMIT 10`,
|
|
1008
|
+
args: [agentId]
|
|
1009
|
+
});
|
|
1010
|
+
if (openTasks.rows.length === 0) {
|
|
1011
|
+
process.stderr.write(
|
|
1012
|
+
`[capacity-monitor] ${agentId} has no open tasks \u2014 skipping relaunch.
|
|
1013
|
+
`
|
|
1014
|
+
);
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
const { created } = await createOrRefreshResumeTask(
|
|
1018
|
+
agentId,
|
|
1019
|
+
projectDir,
|
|
1020
|
+
openTasks.rows
|
|
1021
|
+
);
|
|
1022
|
+
if (created) {
|
|
1023
|
+
await writeNotification({
|
|
1024
|
+
agentId: "system",
|
|
1025
|
+
agentRole: "daemon",
|
|
1026
|
+
event: "capacity_relaunch",
|
|
1027
|
+
project: projectDir.split("/").pop() ?? "unknown",
|
|
1028
|
+
summary: `${agentId} hit context capacity. Auto-relaunched with ${openTasks.rows.length} open task(s).`
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
_lastRelaunch.set(agentId, Date.now());
|
|
1032
|
+
if (created) relaunched.push(agentId);
|
|
1033
|
+
} catch (err) {
|
|
1034
|
+
process.stderr.write(
|
|
1035
|
+
`[capacity-monitor] Failed to relaunch ${agentId}: ${err instanceof Error ? err.message : String(err)}
|
|
1036
|
+
`
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
return relaunched;
|
|
913
1041
|
}
|
|
914
|
-
var
|
|
915
|
-
var
|
|
916
|
-
"src/lib/
|
|
1042
|
+
var CAPACITY_PATTERNS, CONTENT_LINE_PREFIX, CONTENT_LINE_MARKERS, SOURCE_CODE_MARKERS, RELAUNCH_COOLDOWN_MS, _lastRelaunch, RESUME_TITLE_PREFIX, RESUME_TITLE_LIKE_PATTERN, RESUME_ACTIVE_STATUSES, CONFIRMATION_WINDOW_MS, _pendingCapacityKill, CC_CONTEXT_BAR_RE, CTX_FLOOR_PERCENT;
|
|
1043
|
+
var init_capacity_monitor = __esm({
|
|
1044
|
+
"src/lib/capacity-monitor.ts"() {
|
|
917
1045
|
"use strict";
|
|
918
|
-
|
|
1046
|
+
init_session_registry();
|
|
1047
|
+
init_transport();
|
|
1048
|
+
init_notifications();
|
|
1049
|
+
init_database();
|
|
1050
|
+
init_session_kill_telemetry();
|
|
1051
|
+
init_tmux_routing();
|
|
1052
|
+
CAPACITY_PATTERNS = [
|
|
1053
|
+
/conversation is too long/i,
|
|
1054
|
+
/maximum context length/i,
|
|
1055
|
+
/context window.*(?:limit|exceed|full)/i,
|
|
1056
|
+
/reached.*(?:token|context).*limit/i
|
|
1057
|
+
];
|
|
1058
|
+
CONTENT_LINE_PREFIX = /^[\s>#\-*[]/;
|
|
1059
|
+
CONTENT_LINE_MARKERS = [
|
|
1060
|
+
"RESUME:",
|
|
1061
|
+
"intercom",
|
|
1062
|
+
"capacity-monitor",
|
|
1063
|
+
"CAPACITY_PATTERNS",
|
|
1064
|
+
"isAtCapacity",
|
|
1065
|
+
"CONTENT_LINE_MARKERS",
|
|
1066
|
+
"pollCapacityDead",
|
|
1067
|
+
"confirmCapacityKill",
|
|
1068
|
+
"session_kills",
|
|
1069
|
+
"capacity-monitor.test"
|
|
1070
|
+
];
|
|
1071
|
+
SOURCE_CODE_MARKERS = [
|
|
1072
|
+
/["'`/].*(?:maximum context length|conversation is too long)/i,
|
|
1073
|
+
/(?:maximum context length|conversation is too long).*["'`/]/i
|
|
1074
|
+
];
|
|
1075
|
+
RELAUNCH_COOLDOWN_MS = 5 * 60 * 1e3;
|
|
1076
|
+
_lastRelaunch = /* @__PURE__ */ new Map();
|
|
1077
|
+
RESUME_TITLE_PREFIX = "RESUME:";
|
|
1078
|
+
RESUME_TITLE_LIKE_PATTERN = `${RESUME_TITLE_PREFIX} % hit context capacity%`;
|
|
1079
|
+
RESUME_ACTIVE_STATUSES = ["open", "in_progress"];
|
|
1080
|
+
CONFIRMATION_WINDOW_MS = 3 * 60 * 1e3;
|
|
1081
|
+
_pendingCapacityKill = /* @__PURE__ */ new Map();
|
|
1082
|
+
CC_CONTEXT_BAR_RE = /([\u2588\u2591\u2592\u2593]{10})\s+(\d+)%/;
|
|
1083
|
+
CTX_FLOOR_PERCENT = 50;
|
|
919
1084
|
}
|
|
920
1085
|
});
|
|
921
1086
|
|
|
922
|
-
// src/lib/
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1087
|
+
// src/lib/tmux-routing.ts
|
|
1088
|
+
var tmux_routing_exports = {};
|
|
1089
|
+
__export(tmux_routing_exports, {
|
|
1090
|
+
acquireSpawnLock: () => acquireSpawnLock,
|
|
1091
|
+
employeeSessionName: () => employeeSessionName,
|
|
1092
|
+
ensureEmployee: () => ensureEmployee,
|
|
1093
|
+
extractRootExe: () => extractRootExe,
|
|
1094
|
+
findFreeInstance: () => findFreeInstance,
|
|
1095
|
+
getDispatchedBy: () => getDispatchedBy,
|
|
1096
|
+
getMySession: () => getMySession,
|
|
1097
|
+
getParentExe: () => getParentExe,
|
|
1098
|
+
getSessionState: () => getSessionState,
|
|
1099
|
+
isEmployeeAlive: () => isEmployeeAlive,
|
|
1100
|
+
isExeSession: () => isExeSession,
|
|
1101
|
+
isSessionBusy: () => isSessionBusy,
|
|
1102
|
+
notifyParentExe: () => notifyParentExe,
|
|
1103
|
+
parseParentExe: () => parseParentExe,
|
|
1104
|
+
registerParentExe: () => registerParentExe,
|
|
1105
|
+
releaseSpawnLock: () => releaseSpawnLock,
|
|
1106
|
+
resolveExeSession: () => resolveExeSession,
|
|
1107
|
+
sendIntercom: () => sendIntercom,
|
|
1108
|
+
spawnEmployee: () => spawnEmployee,
|
|
1109
|
+
verifyPaneAtCapacity: () => verifyPaneAtCapacity
|
|
1110
|
+
});
|
|
1111
|
+
import { execFileSync as execFileSync2, execSync as execSync4 } from "child_process";
|
|
1112
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, existsSync as existsSync8, appendFileSync } from "fs";
|
|
1113
|
+
import path8 from "path";
|
|
1114
|
+
import os5 from "os";
|
|
1115
|
+
import { fileURLToPath } from "url";
|
|
1116
|
+
import { unlinkSync as unlinkSync2 } from "fs";
|
|
1117
|
+
function spawnLockPath(sessionName) {
|
|
1118
|
+
return path8.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
|
|
1119
|
+
}
|
|
1120
|
+
function isProcessAlive(pid) {
|
|
1121
|
+
try {
|
|
1122
|
+
process.kill(pid, 0);
|
|
1123
|
+
return true;
|
|
1124
|
+
} catch {
|
|
1125
|
+
return false;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
function acquireSpawnLock(sessionName) {
|
|
1129
|
+
if (!existsSync8(SPAWN_LOCK_DIR)) {
|
|
1130
|
+
mkdirSync4(SPAWN_LOCK_DIR, { recursive: true });
|
|
1131
|
+
}
|
|
1132
|
+
const lockFile = spawnLockPath(sessionName);
|
|
1133
|
+
if (existsSync8(lockFile)) {
|
|
1134
|
+
try {
|
|
1135
|
+
const lock = JSON.parse(readFileSync8(lockFile, "utf8"));
|
|
1136
|
+
const age = Date.now() - lock.timestamp;
|
|
1137
|
+
if (isProcessAlive(lock.pid) && age < 6e4) {
|
|
1138
|
+
return false;
|
|
1139
|
+
}
|
|
1140
|
+
} catch {
|
|
928
1141
|
}
|
|
929
1142
|
}
|
|
930
|
-
|
|
1143
|
+
writeFileSync4(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
|
|
1144
|
+
return true;
|
|
931
1145
|
}
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
MCP_PRIMARY_KEY = "exe-os";
|
|
937
|
-
MCP_LEGACY_KEY = "exe-mem";
|
|
938
|
-
MCP_TOOL_PREFIXES = [
|
|
939
|
-
`mcp__${MCP_PRIMARY_KEY}__`,
|
|
940
|
-
`mcp__${MCP_LEGACY_KEY}__`
|
|
941
|
-
];
|
|
1146
|
+
function releaseSpawnLock(sessionName) {
|
|
1147
|
+
try {
|
|
1148
|
+
unlinkSync2(spawnLockPath(sessionName));
|
|
1149
|
+
} catch {
|
|
942
1150
|
}
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1151
|
+
}
|
|
1152
|
+
function resolveBehaviorsExporterScript() {
|
|
1153
|
+
try {
|
|
1154
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
1155
|
+
const scriptPath = path8.join(
|
|
1156
|
+
path8.dirname(thisFile),
|
|
1157
|
+
"..",
|
|
1158
|
+
"bin",
|
|
1159
|
+
"exe-export-behaviors.js"
|
|
1160
|
+
);
|
|
1161
|
+
return existsSync8(scriptPath) ? scriptPath : null;
|
|
1162
|
+
} catch {
|
|
1163
|
+
return null;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
function exportBehaviorsSync(agentId, projectName, sessionKey) {
|
|
1167
|
+
const script = resolveBehaviorsExporterScript();
|
|
1168
|
+
if (!script) return null;
|
|
1169
|
+
try {
|
|
1170
|
+
const output = execFileSync2(
|
|
1171
|
+
process.execPath,
|
|
1172
|
+
[script, agentId, projectName, sessionKey],
|
|
1173
|
+
{ encoding: "utf-8", timeout: BEHAVIORS_EXPORT_TIMEOUT_MS }
|
|
1174
|
+
).trim();
|
|
1175
|
+
return output.length > 0 ? output : null;
|
|
1176
|
+
} catch (err) {
|
|
1177
|
+
process.stderr.write(
|
|
1178
|
+
`[tmux-routing] behaviors export failed for ${agentId}: ${err instanceof Error ? err.message : String(err)}
|
|
1179
|
+
`
|
|
1180
|
+
);
|
|
1181
|
+
return null;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
function getMySession() {
|
|
1185
|
+
return getTransport().getMySession();
|
|
1186
|
+
}
|
|
1187
|
+
function employeeSessionName(employee, exeSession, instance) {
|
|
1188
|
+
if (!/^exe\d+$/.test(exeSession)) {
|
|
1189
|
+
const root = extractRootExe(exeSession);
|
|
1190
|
+
if (root) {
|
|
1191
|
+
process.stderr.write(
|
|
1192
|
+
`[tmux-routing] WARN: exeSession="${exeSession}" is not a root exe session, using "${root}" instead
|
|
1193
|
+
`
|
|
1194
|
+
);
|
|
1195
|
+
exeSession = root;
|
|
1196
|
+
} else {
|
|
1197
|
+
throw new Error(
|
|
1198
|
+
`Invalid exeSession "${exeSession}" \u2014 must be a root exe session (e.g., "exe1"), not an agent session`
|
|
1199
|
+
);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
const suffix = instance != null && instance > 0 ? String(instance) : "";
|
|
1203
|
+
const name = `${employee}${suffix}-${exeSession}`;
|
|
1204
|
+
if (!VALID_SESSION_NAME.test(name)) {
|
|
1205
|
+
throw new Error(
|
|
1206
|
+
`Invalid session name "${name}" \u2014 must match {agent}-exe{N} or {agent}{instance}-exe{N}`
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
return name;
|
|
1210
|
+
}
|
|
1211
|
+
function parseParentExe(sessionName, agentId) {
|
|
1212
|
+
const escaped = agentId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1213
|
+
const regex = new RegExp(`^${escaped}\\d*-(.+)$`);
|
|
1214
|
+
const match = sessionName.match(regex);
|
|
1215
|
+
return match?.[1] ?? null;
|
|
1216
|
+
}
|
|
1217
|
+
function extractRootExe(name) {
|
|
1218
|
+
const match = name.match(/(exe\d+)$/);
|
|
1219
|
+
return match?.[1] ?? null;
|
|
1220
|
+
}
|
|
1221
|
+
function registerParentExe(sessionKey, parentExe, dispatchedBy) {
|
|
1222
|
+
if (!existsSync8(SESSION_CACHE)) {
|
|
1223
|
+
mkdirSync4(SESSION_CACHE, { recursive: true });
|
|
951
1224
|
}
|
|
952
|
-
|
|
1225
|
+
const rootExe = extractRootExe(parentExe) ?? parentExe;
|
|
1226
|
+
const filePath = path8.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`);
|
|
1227
|
+
writeFileSync4(filePath, JSON.stringify({
|
|
1228
|
+
parentExe: rootExe,
|
|
1229
|
+
dispatchedBy: dispatchedBy || rootExe,
|
|
1230
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1231
|
+
}));
|
|
953
1232
|
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
baseUrl: "https://opencode.ai/zen/go",
|
|
961
|
-
apiKeyEnv: "OPENCODE_API_KEY",
|
|
962
|
-
defaultModel: "minimax-m2.7"
|
|
963
|
-
}
|
|
964
|
-
};
|
|
965
|
-
DEFAULT_PROVIDER = "default";
|
|
1233
|
+
function getParentExe(sessionKey) {
|
|
1234
|
+
try {
|
|
1235
|
+
const data = JSON.parse(readFileSync8(path8.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
|
|
1236
|
+
return data.parentExe || null;
|
|
1237
|
+
} catch {
|
|
1238
|
+
return null;
|
|
966
1239
|
}
|
|
967
|
-
});
|
|
968
|
-
|
|
969
|
-
// src/lib/intercom-queue.ts
|
|
970
|
-
import { readFileSync as readFileSync6, writeFileSync as writeFileSync2, renameSync as renameSync2, existsSync as existsSync6, mkdirSync as mkdirSync2 } from "fs";
|
|
971
|
-
import path6 from "path";
|
|
972
|
-
import os4 from "os";
|
|
973
|
-
function ensureDir() {
|
|
974
|
-
const dir = path6.dirname(QUEUE_PATH);
|
|
975
|
-
if (!existsSync6(dir)) mkdirSync2(dir, { recursive: true });
|
|
976
1240
|
}
|
|
977
|
-
function
|
|
1241
|
+
function getDispatchedBy(sessionKey) {
|
|
978
1242
|
try {
|
|
979
|
-
|
|
980
|
-
|
|
1243
|
+
const data = JSON.parse(readFileSync8(
|
|
1244
|
+
path8.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
|
|
1245
|
+
"utf8"
|
|
1246
|
+
));
|
|
1247
|
+
return data.dispatchedBy ?? data.parentExe ?? null;
|
|
981
1248
|
} catch {
|
|
982
|
-
return
|
|
1249
|
+
return null;
|
|
983
1250
|
}
|
|
984
1251
|
}
|
|
985
|
-
function
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1252
|
+
function resolveExeSession() {
|
|
1253
|
+
const mySession = getMySession();
|
|
1254
|
+
if (!mySession) return null;
|
|
1255
|
+
try {
|
|
1256
|
+
const key = getSessionKey();
|
|
1257
|
+
const parentExe = getParentExe(key);
|
|
1258
|
+
if (parentExe) {
|
|
1259
|
+
return extractRootExe(parentExe) ?? parentExe;
|
|
1260
|
+
}
|
|
1261
|
+
} catch {
|
|
1262
|
+
}
|
|
1263
|
+
return extractRootExe(mySession) ?? mySession;
|
|
990
1264
|
}
|
|
991
|
-
function
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
targetSession,
|
|
1001
|
-
queuedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1002
|
-
attempts: 0,
|
|
1003
|
-
reason
|
|
1004
|
-
});
|
|
1265
|
+
function isEmployeeAlive(sessionName) {
|
|
1266
|
+
return getTransport().isAlive(sessionName);
|
|
1267
|
+
}
|
|
1268
|
+
function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
|
|
1269
|
+
const base = employeeSessionName(employeeName, exeSession);
|
|
1270
|
+
if (!isAlive(base) && acquireSpawnLock(base)) return 0;
|
|
1271
|
+
for (let i = 2; i <= maxInstances; i++) {
|
|
1272
|
+
const candidate = employeeSessionName(employeeName, exeSession, i);
|
|
1273
|
+
if (!isAlive(candidate) && acquireSpawnLock(candidate)) return i;
|
|
1005
1274
|
}
|
|
1006
|
-
|
|
1275
|
+
return null;
|
|
1007
1276
|
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
QUEUE_PATH = path6.join(os4.homedir(), ".exe-os", "intercom-queue.json");
|
|
1013
|
-
TTL_MS = 60 * 60 * 1e3;
|
|
1014
|
-
INTERCOM_LOG = path6.join(os4.homedir(), ".exe-os", "intercom.log");
|
|
1277
|
+
async function verifyPaneAtCapacity(sessionName) {
|
|
1278
|
+
const transport = getTransport();
|
|
1279
|
+
if (!transport.isAlive(sessionName)) {
|
|
1280
|
+
return { atCapacity: false, reason: `session ${sessionName} is not alive` };
|
|
1015
1281
|
}
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
|
|
1024
|
-
var init_license = __esm({
|
|
1025
|
-
"src/lib/license.ts"() {
|
|
1026
|
-
"use strict";
|
|
1027
|
-
init_config();
|
|
1028
|
-
LICENSE_PATH = path7.join(EXE_AI_DIR, "license.key");
|
|
1029
|
-
CACHE_PATH = path7.join(EXE_AI_DIR, "license-cache.json");
|
|
1030
|
-
DEVICE_ID_PATH = path7.join(EXE_AI_DIR, "device-id");
|
|
1031
|
-
PLAN_LIMITS = {
|
|
1032
|
-
free: { devices: 1, employees: 1, memories: 5e3 },
|
|
1033
|
-
pro: { devices: 2, employees: 5, memories: 1e5 },
|
|
1034
|
-
team: { devices: 10, employees: 20, memories: 1e6 },
|
|
1035
|
-
agency: { devices: 50, employees: 100, memories: 1e7 },
|
|
1036
|
-
enterprise: { devices: -1, employees: -1, memories: -1 }
|
|
1282
|
+
let pane;
|
|
1283
|
+
try {
|
|
1284
|
+
pane = transport.capturePane(sessionName, VERIFY_PANE_LINES);
|
|
1285
|
+
} catch (err) {
|
|
1286
|
+
return {
|
|
1287
|
+
atCapacity: false,
|
|
1288
|
+
reason: `capture-pane failed: ${err instanceof Error ? err.message : String(err)}`
|
|
1037
1289
|
};
|
|
1038
1290
|
}
|
|
1039
|
-
});
|
|
1040
|
-
|
|
1041
|
-
// src/lib/plan-limits.ts
|
|
1042
|
-
import { readFileSync as readFileSync8, existsSync as existsSync8 } from "fs";
|
|
1043
|
-
import path8 from "path";
|
|
1044
|
-
function getLicenseSync() {
|
|
1045
|
-
try {
|
|
1046
|
-
if (!existsSync8(CACHE_PATH2)) return freeLicense();
|
|
1047
|
-
const raw = JSON.parse(readFileSync8(CACHE_PATH2, "utf8"));
|
|
1048
|
-
if (!raw.token || typeof raw.token !== "string") return freeLicense();
|
|
1049
|
-
const parts = raw.token.split(".");
|
|
1050
|
-
if (parts.length !== 3) return freeLicense();
|
|
1051
|
-
const payload = JSON.parse(Buffer.from(parts[1], "base64url").toString());
|
|
1052
|
-
const plan = payload.plan ?? "free";
|
|
1053
|
-
const limits = PLAN_LIMITS[plan] ?? PLAN_LIMITS.free;
|
|
1291
|
+
const { isAtCapacity: isAtCapacity2 } = await Promise.resolve().then(() => (init_capacity_monitor(), capacity_monitor_exports));
|
|
1292
|
+
if (!isAtCapacity2(pane)) {
|
|
1054
1293
|
return {
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
email: payload.sub ?? "",
|
|
1058
|
-
expiresAt: payload.exp ? new Date(payload.exp * 1e3).toISOString() : null,
|
|
1059
|
-
deviceLimit: limits.devices,
|
|
1060
|
-
employeeLimit: limits.employees,
|
|
1061
|
-
memoryLimit: limits.memories
|
|
1294
|
+
atCapacity: false,
|
|
1295
|
+
reason: `last ${VERIFY_PANE_LINES} lines show normal work, no capacity banner`
|
|
1062
1296
|
};
|
|
1063
|
-
} catch {
|
|
1064
|
-
return freeLicense();
|
|
1065
1297
|
}
|
|
1066
|
-
}
|
|
1067
|
-
function freeLicense() {
|
|
1068
|
-
const limits = PLAN_LIMITS.free;
|
|
1069
1298
|
return {
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
email: "",
|
|
1073
|
-
expiresAt: null,
|
|
1074
|
-
deviceLimit: limits.devices,
|
|
1075
|
-
employeeLimit: limits.employees,
|
|
1076
|
-
memoryLimit: limits.memories
|
|
1299
|
+
atCapacity: true,
|
|
1300
|
+
reason: "capacity banner matched in recent pane output"
|
|
1077
1301
|
};
|
|
1078
1302
|
}
|
|
1079
|
-
function
|
|
1080
|
-
const license = getLicenseSync();
|
|
1081
|
-
if (license.employeeLimit < 0) return;
|
|
1082
|
-
const filePath = rosterPath ?? EMPLOYEES_PATH;
|
|
1083
|
-
let count = 0;
|
|
1303
|
+
function readDebounceState() {
|
|
1084
1304
|
try {
|
|
1085
|
-
if (existsSync8(
|
|
1086
|
-
|
|
1087
|
-
const employees = JSON.parse(raw);
|
|
1088
|
-
count = Array.isArray(employees) ? employees.length : 0;
|
|
1089
|
-
}
|
|
1305
|
+
if (!existsSync8(DEBOUNCE_FILE)) return {};
|
|
1306
|
+
return JSON.parse(readFileSync8(DEBOUNCE_FILE, "utf8"));
|
|
1090
1307
|
} catch {
|
|
1091
|
-
|
|
1092
|
-
`Cannot verify employee count: roster unreadable at ${filePath}. Refusing to proceed. Check file permissions or upgrade plan.`
|
|
1093
|
-
);
|
|
1308
|
+
return {};
|
|
1094
1309
|
}
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
);
|
|
1310
|
+
}
|
|
1311
|
+
function writeDebounceState(state) {
|
|
1312
|
+
try {
|
|
1313
|
+
if (!existsSync8(SESSION_CACHE)) mkdirSync4(SESSION_CACHE, { recursive: true });
|
|
1314
|
+
writeFileSync4(DEBOUNCE_FILE, JSON.stringify(state));
|
|
1315
|
+
} catch {
|
|
1099
1316
|
}
|
|
1100
1317
|
}
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1318
|
+
function isDebounced(targetSession) {
|
|
1319
|
+
const state = readDebounceState();
|
|
1320
|
+
const lastSent = state[targetSession] ?? 0;
|
|
1321
|
+
return Date.now() - lastSent < INTERCOM_DEBOUNCE_MS;
|
|
1322
|
+
}
|
|
1323
|
+
function recordDebounce(targetSession) {
|
|
1324
|
+
const state = readDebounceState();
|
|
1325
|
+
state[targetSession] = Date.now();
|
|
1326
|
+
const cutoff = Date.now() - DEBOUNCE_CLEANUP_AGE_MS;
|
|
1327
|
+
for (const key of Object.keys(state)) {
|
|
1328
|
+
if ((state[key] ?? 0) < cutoff) delete state[key];
|
|
1329
|
+
}
|
|
1330
|
+
writeDebounceState(state);
|
|
1331
|
+
}
|
|
1332
|
+
function logIntercom(msg) {
|
|
1333
|
+
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] ${msg}
|
|
1334
|
+
`;
|
|
1335
|
+
process.stderr.write(`[intercom] ${msg}
|
|
1336
|
+
`);
|
|
1337
|
+
try {
|
|
1338
|
+
appendFileSync(INTERCOM_LOG2, line);
|
|
1339
|
+
} catch {
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
function getSessionState(sessionName) {
|
|
1343
|
+
const transport = getTransport();
|
|
1344
|
+
if (!transport.isAlive(sessionName)) return "offline";
|
|
1345
|
+
try {
|
|
1346
|
+
const pane = transport.capturePane(sessionName, 5);
|
|
1347
|
+
if (!pane.includes("\u276F") && !pane.includes("Claude Code") && !BUSY_PATTERN.test(pane) && !/Running…/.test(pane)) {
|
|
1348
|
+
if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
|
|
1349
|
+
return "no_claude";
|
|
1113
1350
|
}
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1351
|
+
}
|
|
1352
|
+
if (/Running…/.test(pane)) return "tool";
|
|
1353
|
+
if (BUSY_PATTERN.test(pane)) return "thinking";
|
|
1354
|
+
return "idle";
|
|
1355
|
+
} catch {
|
|
1356
|
+
return "offline";
|
|
1116
1357
|
}
|
|
1117
|
-
});
|
|
1118
|
-
|
|
1119
|
-
// src/lib/tmux-routing.ts
|
|
1120
|
-
import { execFileSync as execFileSync2, execSync as execSync5 } from "child_process";
|
|
1121
|
-
import { readFileSync as readFileSync9, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, existsSync as existsSync9, appendFileSync } from "fs";
|
|
1122
|
-
import path9 from "path";
|
|
1123
|
-
import os5 from "os";
|
|
1124
|
-
import { fileURLToPath } from "url";
|
|
1125
|
-
import { unlinkSync as unlinkSync2 } from "fs";
|
|
1126
|
-
function spawnLockPath(sessionName) {
|
|
1127
|
-
return path9.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
|
|
1128
1358
|
}
|
|
1129
|
-
function
|
|
1359
|
+
function isSessionBusy(sessionName) {
|
|
1360
|
+
const state = getSessionState(sessionName);
|
|
1361
|
+
return state === "thinking" || state === "tool";
|
|
1362
|
+
}
|
|
1363
|
+
function isExeSession(sessionName) {
|
|
1364
|
+
return /^exe\d*$/.test(sessionName);
|
|
1365
|
+
}
|
|
1366
|
+
function sendIntercom(targetSession) {
|
|
1367
|
+
const transport = getTransport();
|
|
1368
|
+
if (isExeSession(targetSession)) {
|
|
1369
|
+
logIntercom(`SKIP_EXE \u2192 ${targetSession} (exe sessions use prompt-submit hook)`);
|
|
1370
|
+
return "skipped_exe";
|
|
1371
|
+
}
|
|
1372
|
+
if (isDebounced(targetSession)) {
|
|
1373
|
+
logIntercom(`DEBOUNCE \u2192 ${targetSession} (cross-process file debounce)`);
|
|
1374
|
+
return "debounced";
|
|
1375
|
+
}
|
|
1130
1376
|
try {
|
|
1131
|
-
|
|
1132
|
-
|
|
1377
|
+
const sessions = transport.listSessions();
|
|
1378
|
+
if (!sessions.includes(targetSession)) {
|
|
1379
|
+
logIntercom(`SKIP \u2192 ${targetSession} (session not found)`);
|
|
1380
|
+
return "failed";
|
|
1381
|
+
}
|
|
1382
|
+
const sessionState = getSessionState(targetSession);
|
|
1383
|
+
if (sessionState === "no_claude") {
|
|
1384
|
+
queueIntercom(targetSession, "claude not running in session");
|
|
1385
|
+
recordDebounce(targetSession);
|
|
1386
|
+
logIntercom(`QUEUED \u2192 ${targetSession} (no claude process \u2014 raw shell detected)`);
|
|
1387
|
+
return "queued";
|
|
1388
|
+
}
|
|
1389
|
+
if (sessionState === "thinking" || sessionState === "tool") {
|
|
1390
|
+
queueIntercom(targetSession, "session busy at send time");
|
|
1391
|
+
recordDebounce(targetSession);
|
|
1392
|
+
logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
|
|
1393
|
+
return "queued";
|
|
1394
|
+
}
|
|
1395
|
+
if (transport.isPaneInCopyMode(targetSession)) {
|
|
1396
|
+
logIntercom(`COPY_MODE \u2192 ${targetSession} (exiting copy mode first)`);
|
|
1397
|
+
transport.sendKeys(targetSession, "q");
|
|
1398
|
+
}
|
|
1399
|
+
transport.sendKeys(targetSession, "/exe-intercom");
|
|
1400
|
+
recordDebounce(targetSession);
|
|
1401
|
+
logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
|
|
1402
|
+
return "delivered";
|
|
1133
1403
|
} catch {
|
|
1134
|
-
|
|
1404
|
+
logIntercom(`FAIL \u2192 ${targetSession}`);
|
|
1405
|
+
return "failed";
|
|
1135
1406
|
}
|
|
1136
1407
|
}
|
|
1137
|
-
function
|
|
1138
|
-
|
|
1139
|
-
|
|
1408
|
+
function notifyParentExe(sessionKey) {
|
|
1409
|
+
const target = getDispatchedBy(sessionKey);
|
|
1410
|
+
if (!target) {
|
|
1411
|
+
process.stderr.write(`[intercom] notifyParentExe: no dispatcher found for key ${sessionKey}
|
|
1412
|
+
`);
|
|
1413
|
+
return false;
|
|
1140
1414
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1415
|
+
process.stderr.write(`[intercom] notifyParentExe \u2192 ${target}
|
|
1416
|
+
`);
|
|
1417
|
+
const result = sendIntercom(target);
|
|
1418
|
+
if (result === "failed") {
|
|
1419
|
+
const rootExe = resolveExeSession();
|
|
1420
|
+
if (rootExe && rootExe !== target) {
|
|
1421
|
+
process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
|
|
1422
|
+
`);
|
|
1423
|
+
const fallback = sendIntercom(rootExe);
|
|
1424
|
+
return fallback !== "failed";
|
|
1150
1425
|
}
|
|
1426
|
+
return false;
|
|
1151
1427
|
}
|
|
1152
|
-
writeFileSync4(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
|
|
1153
1428
|
return true;
|
|
1154
1429
|
}
|
|
1155
|
-
function
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
} catch {
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
function resolveBehaviorsExporterScript() {
|
|
1162
|
-
try {
|
|
1163
|
-
const thisFile = fileURLToPath(import.meta.url);
|
|
1164
|
-
const scriptPath = path9.join(
|
|
1165
|
-
path9.dirname(thisFile),
|
|
1166
|
-
"..",
|
|
1167
|
-
"bin",
|
|
1168
|
-
"exe-export-behaviors.js"
|
|
1169
|
-
);
|
|
1170
|
-
return existsSync9(scriptPath) ? scriptPath : null;
|
|
1171
|
-
} catch {
|
|
1172
|
-
return null;
|
|
1430
|
+
function ensureEmployee(employeeName, exeSession, projectDir, opts) {
|
|
1431
|
+
if (employeeName === "exe") {
|
|
1432
|
+
return { status: "failed", sessionName: "", error: "exe is the COO, not a dispatchable employee" };
|
|
1173
1433
|
}
|
|
1174
|
-
}
|
|
1175
|
-
function exportBehaviorsSync(agentId, projectName, sessionKey) {
|
|
1176
|
-
const script = resolveBehaviorsExporterScript();
|
|
1177
|
-
if (!script) return null;
|
|
1178
1434
|
try {
|
|
1179
|
-
|
|
1180
|
-
process.execPath,
|
|
1181
|
-
[script, agentId, projectName, sessionKey],
|
|
1182
|
-
{ encoding: "utf-8", timeout: BEHAVIORS_EXPORT_TIMEOUT_MS }
|
|
1183
|
-
).trim();
|
|
1184
|
-
return output.length > 0 ? output : null;
|
|
1435
|
+
assertEmployeeLimitSync();
|
|
1185
1436
|
} catch (err) {
|
|
1186
|
-
|
|
1187
|
-
|
|
1437
|
+
if (err instanceof PlanLimitError) {
|
|
1438
|
+
return { status: "failed", sessionName: "", error: err.message };
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
if (/-exe\d*$/.test(employeeName)) {
|
|
1442
|
+
const bare = employeeName.replace(/-exe\d*$/, "").replace(/\d+$/, "");
|
|
1443
|
+
return {
|
|
1444
|
+
status: "failed",
|
|
1445
|
+
sessionName: "",
|
|
1446
|
+
error: `Error: pass employee name ('${bare}'), not session name ('${employeeName}')`
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
if (!/^exe\d+$/.test(exeSession)) {
|
|
1450
|
+
const root = extractRootExe(exeSession);
|
|
1451
|
+
if (root) {
|
|
1452
|
+
process.stderr.write(
|
|
1453
|
+
`[ensureEmployee] WARN: caller passed exeSession="${exeSession}" (not a root exe). Auto-correcting to "${root}".
|
|
1188
1454
|
`
|
|
1455
|
+
);
|
|
1456
|
+
exeSession = root;
|
|
1457
|
+
} else {
|
|
1458
|
+
return {
|
|
1459
|
+
status: "failed",
|
|
1460
|
+
sessionName: "",
|
|
1461
|
+
error: `Invalid exeSession "${exeSession}" \u2014 must be a root exe session (e.g., "exe1")`
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
let effectiveInstance = opts?.instance;
|
|
1466
|
+
if (effectiveInstance === void 0 && opts?.autoInstance) {
|
|
1467
|
+
const free = findFreeInstance(
|
|
1468
|
+
employeeName,
|
|
1469
|
+
exeSession,
|
|
1470
|
+
opts.maxAutoInstances ?? 10
|
|
1189
1471
|
);
|
|
1190
|
-
|
|
1472
|
+
if (free === null) {
|
|
1473
|
+
return {
|
|
1474
|
+
status: "failed",
|
|
1475
|
+
sessionName: employeeSessionName(employeeName, exeSession),
|
|
1476
|
+
error: `All ${opts.maxAutoInstances ?? 10} instances of ${employeeName} are alive \u2014 cap reached`
|
|
1477
|
+
};
|
|
1478
|
+
}
|
|
1479
|
+
effectiveInstance = free === 0 ? void 0 : free;
|
|
1191
1480
|
}
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
return match?.[1] ?? null;
|
|
1203
|
-
}
|
|
1204
|
-
function getParentExe(sessionKey) {
|
|
1205
|
-
try {
|
|
1206
|
-
const data = JSON.parse(readFileSync9(path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`), "utf8"));
|
|
1207
|
-
return data.parentExe || null;
|
|
1208
|
-
} catch {
|
|
1209
|
-
return null;
|
|
1481
|
+
const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
|
|
1482
|
+
if (isEmployeeAlive(sessionName)) {
|
|
1483
|
+
const result2 = sendIntercom(sessionName);
|
|
1484
|
+
if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
|
|
1485
|
+
return { status: "intercom_sent", sessionName };
|
|
1486
|
+
}
|
|
1487
|
+
if (result2 === "delivered") {
|
|
1488
|
+
return { status: "intercom_unprocessed", sessionName };
|
|
1489
|
+
}
|
|
1490
|
+
return { status: "failed", sessionName, error: "intercom delivery failed" };
|
|
1210
1491
|
}
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
path9.join(SESSION_CACHE, `parent-exe-${sessionKey}.json`),
|
|
1216
|
-
"utf8"
|
|
1217
|
-
));
|
|
1218
|
-
return data.dispatchedBy ?? data.parentExe ?? null;
|
|
1219
|
-
} catch {
|
|
1220
|
-
return null;
|
|
1492
|
+
const spawnOpts = { ...opts, instance: effectiveInstance };
|
|
1493
|
+
const result = spawnEmployee(employeeName, exeSession, projectDir, spawnOpts);
|
|
1494
|
+
if (result.error) {
|
|
1495
|
+
return { status: "failed", sessionName, error: result.error };
|
|
1221
1496
|
}
|
|
1497
|
+
return { status: "spawned", sessionName };
|
|
1222
1498
|
}
|
|
1223
|
-
function
|
|
1224
|
-
const
|
|
1225
|
-
|
|
1499
|
+
function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
1500
|
+
const transport = getTransport();
|
|
1501
|
+
const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
|
|
1502
|
+
const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
|
|
1503
|
+
const logDir = path8.join(os5.homedir(), ".exe-os", "session-logs");
|
|
1504
|
+
const logFile = path8.join(logDir, `${instanceLabel}-${Date.now()}.log`);
|
|
1505
|
+
if (!existsSync8(logDir)) {
|
|
1506
|
+
mkdirSync4(logDir, { recursive: true });
|
|
1507
|
+
}
|
|
1508
|
+
transport.kill(sessionName);
|
|
1509
|
+
let cleanupSuffix = "";
|
|
1226
1510
|
try {
|
|
1227
|
-
const
|
|
1228
|
-
const
|
|
1229
|
-
if (
|
|
1230
|
-
|
|
1511
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
1512
|
+
const cleanupScript = path8.join(path8.dirname(thisFile), "..", "bin", "exe-session-cleanup.js");
|
|
1513
|
+
if (existsSync8(cleanupScript)) {
|
|
1514
|
+
cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
|
|
1231
1515
|
}
|
|
1232
1516
|
} catch {
|
|
1233
1517
|
}
|
|
1234
|
-
return extractRootExe(mySession) ?? mySession;
|
|
1235
|
-
}
|
|
1236
|
-
function isEmployeeAlive(sessionName) {
|
|
1237
|
-
return getTransport().isAlive(sessionName);
|
|
1238
|
-
}
|
|
1239
|
-
function findFreeInstance(employeeName, exeSession, maxInstances = 10, isAlive = isEmployeeAlive) {
|
|
1240
|
-
const base = employeeSessionName(employeeName, exeSession);
|
|
1241
|
-
if (!isAlive(base) && acquireSpawnLock(base)) return 0;
|
|
1242
|
-
for (let i = 2; i <= maxInstances; i++) {
|
|
1243
|
-
const candidate = employeeSessionName(employeeName, exeSession, i);
|
|
1244
|
-
if (!isAlive(candidate) && acquireSpawnLock(candidate)) return i;
|
|
1245
|
-
}
|
|
1246
|
-
return null;
|
|
1247
|
-
}
|
|
1248
|
-
function readDebounceState() {
|
|
1249
1518
|
try {
|
|
1250
|
-
|
|
1251
|
-
|
|
1519
|
+
const claudeJsonPath = path8.join(os5.homedir(), ".claude.json");
|
|
1520
|
+
let claudeJson = {};
|
|
1521
|
+
try {
|
|
1522
|
+
claudeJson = JSON.parse(readFileSync8(claudeJsonPath, "utf8"));
|
|
1523
|
+
} catch {
|
|
1524
|
+
}
|
|
1525
|
+
if (!claudeJson.projects) claudeJson.projects = {};
|
|
1526
|
+
const projects = claudeJson.projects;
|
|
1527
|
+
const trustDir = opts?.cwd ?? projectDir;
|
|
1528
|
+
if (!projects[trustDir]) projects[trustDir] = {};
|
|
1529
|
+
projects[trustDir].hasTrustDialogAccepted = true;
|
|
1530
|
+
writeFileSync4(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
|
|
1252
1531
|
} catch {
|
|
1253
|
-
return {};
|
|
1254
1532
|
}
|
|
1255
|
-
}
|
|
1256
|
-
function writeDebounceState(state) {
|
|
1257
1533
|
try {
|
|
1258
|
-
|
|
1259
|
-
|
|
1534
|
+
const settingsDir = path8.join(os5.homedir(), ".claude", "projects");
|
|
1535
|
+
const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
|
|
1536
|
+
const projSettingsDir = path8.join(settingsDir, normalizedKey);
|
|
1537
|
+
const settingsPath = path8.join(projSettingsDir, "settings.json");
|
|
1538
|
+
let settings = {};
|
|
1539
|
+
try {
|
|
1540
|
+
settings = JSON.parse(readFileSync8(settingsPath, "utf8"));
|
|
1541
|
+
} catch {
|
|
1542
|
+
}
|
|
1543
|
+
const perms = settings.permissions ?? {};
|
|
1544
|
+
const allow = perms.allow ?? [];
|
|
1545
|
+
const toolNames = [
|
|
1546
|
+
"recall_my_memory",
|
|
1547
|
+
"store_memory",
|
|
1548
|
+
"create_task",
|
|
1549
|
+
"update_task",
|
|
1550
|
+
"list_tasks",
|
|
1551
|
+
"get_task",
|
|
1552
|
+
"ask_team_memory",
|
|
1553
|
+
"store_behavior",
|
|
1554
|
+
"get_identity",
|
|
1555
|
+
"send_message"
|
|
1556
|
+
];
|
|
1557
|
+
const requiredTools = expandDualPrefixTools(toolNames);
|
|
1558
|
+
let changed = false;
|
|
1559
|
+
for (const tool of requiredTools) {
|
|
1560
|
+
if (!allow.includes(tool)) {
|
|
1561
|
+
allow.push(tool);
|
|
1562
|
+
changed = true;
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
if (changed) {
|
|
1566
|
+
perms.allow = allow;
|
|
1567
|
+
settings.permissions = perms;
|
|
1568
|
+
mkdirSync4(projSettingsDir, { recursive: true });
|
|
1569
|
+
writeFileSync4(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
1570
|
+
}
|
|
1260
1571
|
} catch {
|
|
1261
1572
|
}
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
const
|
|
1265
|
-
const
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1573
|
+
const spawnCwd = opts?.cwd ?? projectDir;
|
|
1574
|
+
const useExeAgent = !!(opts?.model && opts?.provider);
|
|
1575
|
+
const ccProvider = useExeAgent ? DEFAULT_PROVIDER : detectActiveProvider();
|
|
1576
|
+
const useBinSymlink = ccProvider !== DEFAULT_PROVIDER;
|
|
1577
|
+
let identityFlag = "";
|
|
1578
|
+
let behaviorsFlag = "";
|
|
1579
|
+
let legacyFallbackWarned = false;
|
|
1580
|
+
if (!useExeAgent && !useBinSymlink) {
|
|
1581
|
+
const identityPath = path8.join(
|
|
1582
|
+
os5.homedir(),
|
|
1583
|
+
".exe-os",
|
|
1584
|
+
"identity",
|
|
1585
|
+
`${employeeName}.md`
|
|
1586
|
+
);
|
|
1587
|
+
_resetCcAgentSupportCache();
|
|
1588
|
+
const hasAgentFlag = claudeSupportsAgentFlag();
|
|
1589
|
+
if (hasAgentFlag) {
|
|
1590
|
+
identityFlag = ` --agent ${employeeName}`;
|
|
1591
|
+
} else if (existsSync8(identityPath)) {
|
|
1592
|
+
identityFlag = ` --append-system-prompt-file ${identityPath}`;
|
|
1593
|
+
legacyFallbackWarned = true;
|
|
1594
|
+
}
|
|
1595
|
+
const behaviorsFile = exportBehaviorsSync(
|
|
1596
|
+
employeeName,
|
|
1597
|
+
path8.basename(spawnCwd),
|
|
1598
|
+
sessionName
|
|
1599
|
+
);
|
|
1600
|
+
if (behaviorsFile) {
|
|
1601
|
+
behaviorsFlag = ` --append-system-prompt-file ${behaviorsFile}`;
|
|
1602
|
+
}
|
|
1274
1603
|
}
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1604
|
+
if (legacyFallbackWarned) {
|
|
1605
|
+
process.stderr.write(
|
|
1606
|
+
`[tmux-routing] claude --agent not supported by installed CC. Falling back to --append-system-prompt-file for ${employeeName}. Upgrade Claude Code to enable native --agent launch.
|
|
1607
|
+
`
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
let sessionContextFlag = "";
|
|
1282
1611
|
try {
|
|
1283
|
-
|
|
1612
|
+
const ctxDir = path8.join(os5.homedir(), ".exe-os", "session-cache");
|
|
1613
|
+
mkdirSync4(ctxDir, { recursive: true });
|
|
1614
|
+
const ctxFile = path8.join(ctxDir, `session-context-${sessionName}.md`);
|
|
1615
|
+
const ctxContent = [
|
|
1616
|
+
`## Session Context`,
|
|
1617
|
+
`You are running in tmux session: ${sessionName}.`,
|
|
1618
|
+
`Your parent exe session is ${exeSession}.`,
|
|
1619
|
+
`Your employees (if any) use the -${exeSession} suffix (e.g., tom-${exeSession}).`
|
|
1620
|
+
].join("\n");
|
|
1621
|
+
writeFileSync4(ctxFile, ctxContent);
|
|
1622
|
+
sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
|
|
1284
1623
|
} catch {
|
|
1285
1624
|
}
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
if (/\$\s*$/.test(pane) || /% $/.test(pane.trimEnd())) {
|
|
1294
|
-
return "no_claude";
|
|
1625
|
+
let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
|
|
1626
|
+
if (ccProvider !== DEFAULT_PROVIDER) {
|
|
1627
|
+
const cfg = PROVIDER_TABLE[ccProvider];
|
|
1628
|
+
if (cfg?.apiKeyEnv) {
|
|
1629
|
+
const keyVal = process.env[cfg.apiKeyEnv];
|
|
1630
|
+
if (keyVal) {
|
|
1631
|
+
envPrefix = `${envPrefix} ${cfg.apiKeyEnv}=${keyVal}`;
|
|
1295
1632
|
}
|
|
1296
1633
|
}
|
|
1297
|
-
if (/Running…/.test(pane)) return "tool";
|
|
1298
|
-
if (BUSY_PATTERN.test(pane)) return "thinking";
|
|
1299
|
-
return "idle";
|
|
1300
|
-
} catch {
|
|
1301
|
-
return "offline";
|
|
1302
1634
|
}
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1635
|
+
let spawnCommand;
|
|
1636
|
+
if (useExeAgent) {
|
|
1637
|
+
spawnCommand = `${envPrefix} exe-agent --employee ${employeeName} --model ${opts.model} --provider ${opts.provider}${cleanupSuffix}`;
|
|
1638
|
+
} else if (useBinSymlink) {
|
|
1639
|
+
const binName = `${employeeName}-${ccProvider}`;
|
|
1640
|
+
process.stderr.write(
|
|
1641
|
+
`[tmux-routing] provider cascade: ${ccProvider} \u2192 spawning ${binName}
|
|
1642
|
+
`
|
|
1643
|
+
);
|
|
1644
|
+
spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
|
|
1645
|
+
} else {
|
|
1646
|
+
spawnCommand = `${envPrefix} claude --dangerously-skip-permissions${identityFlag}${behaviorsFlag}${sessionContextFlag}${cleanupSuffix}`;
|
|
1312
1647
|
}
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1648
|
+
const spawnResult = transport.spawn(sessionName, {
|
|
1649
|
+
cwd: spawnCwd,
|
|
1650
|
+
command: spawnCommand
|
|
1651
|
+
});
|
|
1652
|
+
if (spawnResult.error) {
|
|
1653
|
+
releaseSpawnLock(sessionName);
|
|
1654
|
+
return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
|
|
1316
1655
|
}
|
|
1656
|
+
transport.pipeLog(sessionName, logFile);
|
|
1317
1657
|
try {
|
|
1318
|
-
const
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
logIntercom(`QUEUED \u2192 ${targetSession} (session busy, will retry from queue)`);
|
|
1334
|
-
return "queued";
|
|
1658
|
+
const mySession = getMySession();
|
|
1659
|
+
const dispatchInfo = path8.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
|
|
1660
|
+
writeFileSync4(dispatchInfo, JSON.stringify({
|
|
1661
|
+
dispatchedBy: mySession,
|
|
1662
|
+
rootExe: exeSession,
|
|
1663
|
+
provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
|
|
1664
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1665
|
+
}));
|
|
1666
|
+
} catch {
|
|
1667
|
+
}
|
|
1668
|
+
let booted = false;
|
|
1669
|
+
for (let i = 0; i < 30; i++) {
|
|
1670
|
+
try {
|
|
1671
|
+
execSync4("sleep 0.5");
|
|
1672
|
+
} catch {
|
|
1335
1673
|
}
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1674
|
+
try {
|
|
1675
|
+
const pane = transport.capturePane(sessionName);
|
|
1676
|
+
if (useExeAgent) {
|
|
1677
|
+
if (pane.includes("[exe-agent]") || pane.includes("online")) {
|
|
1678
|
+
booted = true;
|
|
1679
|
+
break;
|
|
1680
|
+
}
|
|
1681
|
+
} else {
|
|
1682
|
+
if (pane.includes("Claude Code") || pane.includes("\u276F")) {
|
|
1683
|
+
booted = true;
|
|
1684
|
+
break;
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
} catch {
|
|
1339
1688
|
}
|
|
1340
|
-
transport.sendKeys(targetSession, "/exe-intercom");
|
|
1341
|
-
recordDebounce(targetSession);
|
|
1342
|
-
logIntercom(`DELIVERED \u2192 ${targetSession} (fire-and-forget)`);
|
|
1343
|
-
return "delivered";
|
|
1344
|
-
} catch {
|
|
1345
|
-
logIntercom(`FAIL \u2192 ${targetSession}`);
|
|
1346
|
-
return "failed";
|
|
1347
1689
|
}
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
if (!target) {
|
|
1352
|
-
process.stderr.write(`[intercom] notifyParentExe: no dispatcher found for key ${sessionKey}
|
|
1353
|
-
`);
|
|
1354
|
-
return false;
|
|
1690
|
+
if (!booted) {
|
|
1691
|
+
releaseSpawnLock(sessionName);
|
|
1692
|
+
return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
|
|
1355
1693
|
}
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
const rootExe = resolveExeSession();
|
|
1361
|
-
if (rootExe && rootExe !== target) {
|
|
1362
|
-
process.stderr.write(`[intercom] notifyParentExe: dispatcher ${target} dead, falling back to root exe ${rootExe}
|
|
1363
|
-
`);
|
|
1364
|
-
const fallback = sendIntercom(rootExe);
|
|
1365
|
-
return fallback !== "failed";
|
|
1694
|
+
if (!useExeAgent) {
|
|
1695
|
+
try {
|
|
1696
|
+
transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
|
|
1697
|
+
} catch {
|
|
1366
1698
|
}
|
|
1367
|
-
return false;
|
|
1368
1699
|
}
|
|
1369
|
-
|
|
1700
|
+
registerSession({
|
|
1701
|
+
windowName: sessionName,
|
|
1702
|
+
agentId: employeeName,
|
|
1703
|
+
projectDir: spawnCwd,
|
|
1704
|
+
parentExe: exeSession,
|
|
1705
|
+
pid: 0,
|
|
1706
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1707
|
+
});
|
|
1708
|
+
releaseSpawnLock(sessionName);
|
|
1709
|
+
return { sessionName };
|
|
1370
1710
|
}
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1711
|
+
var SPAWN_LOCK_DIR, SESSION_CACHE, BEHAVIORS_EXPORT_TIMEOUT_MS, VALID_SESSION_NAME, VERIFY_PANE_LINES, INTERCOM_DEBOUNCE_MS, INTERCOM_LOG2, DEBOUNCE_FILE, DEBOUNCE_CLEANUP_AGE_MS, BUSY_PATTERN;
|
|
1712
|
+
var init_tmux_routing = __esm({
|
|
1713
|
+
"src/lib/tmux-routing.ts"() {
|
|
1714
|
+
"use strict";
|
|
1715
|
+
init_session_registry();
|
|
1716
|
+
init_session_key();
|
|
1717
|
+
init_transport();
|
|
1718
|
+
init_cc_agent_support();
|
|
1719
|
+
init_mcp_prefix();
|
|
1720
|
+
init_provider_table();
|
|
1721
|
+
init_intercom_queue();
|
|
1722
|
+
init_plan_limits();
|
|
1723
|
+
SPAWN_LOCK_DIR = path8.join(os5.homedir(), ".exe-os", "spawn-locks");
|
|
1724
|
+
SESSION_CACHE = path8.join(os5.homedir(), ".exe-os", "session-cache");
|
|
1725
|
+
BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
|
|
1726
|
+
VALID_SESSION_NAME = /^[a-z]+-exe\d+$|^[a-z]+\d+-exe\d+$/;
|
|
1727
|
+
VERIFY_PANE_LINES = 200;
|
|
1728
|
+
INTERCOM_DEBOUNCE_MS = 3e4;
|
|
1729
|
+
INTERCOM_LOG2 = path8.join(os5.homedir(), ".exe-os", "intercom.log");
|
|
1730
|
+
DEBOUNCE_FILE = path8.join(SESSION_CACHE, "intercom-debounce.json");
|
|
1731
|
+
DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
|
|
1732
|
+
BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
|
|
1374
1733
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1734
|
+
});
|
|
1735
|
+
|
|
1736
|
+
// src/lib/tasks-crud.ts
|
|
1737
|
+
import crypto3 from "crypto";
|
|
1738
|
+
import path9 from "path";
|
|
1739
|
+
import { execSync as execSync5 } from "child_process";
|
|
1740
|
+
import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
|
|
1741
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
|
|
1742
|
+
async function writeCheckpoint(input) {
|
|
1743
|
+
const client = getClient();
|
|
1744
|
+
const row = await resolveTask(client, input.taskId);
|
|
1745
|
+
const taskId = String(row.id);
|
|
1746
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1747
|
+
const blockedByIds = [];
|
|
1748
|
+
if (row.blocked_by) {
|
|
1749
|
+
blockedByIds.push(String(row.blocked_by));
|
|
1381
1750
|
}
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1751
|
+
const checkpoint = {
|
|
1752
|
+
step: input.step,
|
|
1753
|
+
context_summary: input.contextSummary,
|
|
1754
|
+
files_touched: input.filesTouched ?? [],
|
|
1755
|
+
blocked_by_ids: blockedByIds,
|
|
1756
|
+
last_checkpoint_at: now
|
|
1757
|
+
};
|
|
1758
|
+
const result = await client.execute({
|
|
1759
|
+
sql: `UPDATE tasks SET checkpoint = ?, checkpoint_count = checkpoint_count + 1, updated_at = ? WHERE id = ?`,
|
|
1760
|
+
args: [JSON.stringify(checkpoint), now, taskId]
|
|
1761
|
+
});
|
|
1762
|
+
if (result.rowsAffected === 0) {
|
|
1763
|
+
throw new Error(`Checkpoint write failed: task ${taskId} not found`);
|
|
1389
1764
|
}
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1765
|
+
const countResult = await client.execute({
|
|
1766
|
+
sql: "SELECT checkpoint_count FROM tasks WHERE id = ?",
|
|
1767
|
+
args: [taskId]
|
|
1768
|
+
});
|
|
1769
|
+
const checkpointCount = Number(countResult.rows[0]?.checkpoint_count ?? 1);
|
|
1770
|
+
return { checkpointCount };
|
|
1771
|
+
}
|
|
1772
|
+
function extractParentFromContext(contextBody) {
|
|
1773
|
+
if (!contextBody) return null;
|
|
1774
|
+
const match = contextBody.match(
|
|
1775
|
+
/Parent task:\s*([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i
|
|
1776
|
+
);
|
|
1777
|
+
return match ? match[1].toLowerCase() : null;
|
|
1778
|
+
}
|
|
1779
|
+
function slugify(title) {
|
|
1780
|
+
return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
1781
|
+
}
|
|
1782
|
+
async function resolveTask(client, identifier) {
|
|
1783
|
+
let result = await client.execute({
|
|
1784
|
+
sql: "SELECT * FROM tasks WHERE id = ?",
|
|
1785
|
+
args: [identifier]
|
|
1786
|
+
});
|
|
1787
|
+
if (result.rows.length === 1) return result.rows[0];
|
|
1788
|
+
result = await client.execute({
|
|
1789
|
+
sql: "SELECT * FROM tasks WHERE task_file LIKE ?",
|
|
1790
|
+
args: [`%${identifier}%`]
|
|
1791
|
+
});
|
|
1792
|
+
if (result.rows.length === 1) return result.rows[0];
|
|
1793
|
+
if (result.rows.length > 1) {
|
|
1794
|
+
const exact = result.rows.filter(
|
|
1795
|
+
(r) => String(r.task_file).endsWith(`/${identifier}.md`)
|
|
1796
|
+
);
|
|
1797
|
+
if (exact.length === 1) return exact[0];
|
|
1798
|
+
const candidates = exact.length > 1 ? exact : result.rows;
|
|
1799
|
+
const active = candidates.filter(
|
|
1800
|
+
(r) => !["done", "cancelled"].includes(String(r.status))
|
|
1801
|
+
);
|
|
1802
|
+
if (active.length === 1) return active[0];
|
|
1803
|
+
const matches = (active.length > 1 ? active : candidates).map((r) => `${String(r.task_file)} (${String(r.status)}, ${String(r.id)})`).join(", ");
|
|
1804
|
+
throw new Error(
|
|
1805
|
+
`Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
|
|
1396
1806
|
);
|
|
1397
|
-
if (free === null) {
|
|
1398
|
-
return {
|
|
1399
|
-
status: "failed",
|
|
1400
|
-
sessionName: employeeSessionName(employeeName, exeSession),
|
|
1401
|
-
error: `All ${opts.maxAutoInstances ?? 10} instances of ${employeeName} are alive \u2014 cap reached`
|
|
1402
|
-
};
|
|
1403
|
-
}
|
|
1404
|
-
effectiveInstance = free === 0 ? void 0 : free;
|
|
1405
|
-
}
|
|
1406
|
-
const sessionName = employeeSessionName(employeeName, exeSession, effectiveInstance);
|
|
1407
|
-
if (isEmployeeAlive(sessionName)) {
|
|
1408
|
-
const result2 = sendIntercom(sessionName);
|
|
1409
|
-
if (result2 === "acknowledged" || result2 === "skipped_exe" || result2 === "debounced" || result2 === "queued") {
|
|
1410
|
-
return { status: "intercom_sent", sessionName };
|
|
1411
|
-
}
|
|
1412
|
-
if (result2 === "delivered") {
|
|
1413
|
-
return { status: "intercom_unprocessed", sessionName };
|
|
1414
|
-
}
|
|
1415
|
-
return { status: "failed", sessionName, error: "intercom delivery failed" };
|
|
1416
1807
|
}
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1808
|
+
result = await client.execute({
|
|
1809
|
+
sql: "SELECT * FROM tasks WHERE title LIKE ?",
|
|
1810
|
+
args: [`%${identifier}%`]
|
|
1811
|
+
});
|
|
1812
|
+
if (result.rows.length === 1) return result.rows[0];
|
|
1813
|
+
if (result.rows.length > 1) {
|
|
1814
|
+
const active = result.rows.filter(
|
|
1815
|
+
(r) => !["done", "cancelled"].includes(String(r.status))
|
|
1816
|
+
);
|
|
1817
|
+
if (active.length === 1) return active[0];
|
|
1818
|
+
const matches = (active.length > 1 ? active : result.rows).map((r) => `"${String(r.title)}" (${String(r.status)}, ${String(r.id)})`).join(", ");
|
|
1819
|
+
throw new Error(
|
|
1820
|
+
`Multiple tasks match "${identifier}": ${matches}. Use a UUID to disambiguate.`
|
|
1821
|
+
);
|
|
1421
1822
|
}
|
|
1422
|
-
|
|
1823
|
+
throw new Error(`Task not found: ${identifier}`);
|
|
1423
1824
|
}
|
|
1424
|
-
function
|
|
1425
|
-
const
|
|
1426
|
-
const
|
|
1427
|
-
const
|
|
1428
|
-
const
|
|
1429
|
-
const
|
|
1430
|
-
|
|
1431
|
-
|
|
1825
|
+
async function createTaskCore(input) {
|
|
1826
|
+
const client = getClient();
|
|
1827
|
+
const id = crypto3.randomUUID();
|
|
1828
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1829
|
+
const slug = slugify(input.title);
|
|
1830
|
+
const taskFile = input.taskFile ?? `exe/${input.assignedTo}/${slug}.md`;
|
|
1831
|
+
let blockedById = null;
|
|
1832
|
+
const initialStatus = input.blockedBy ? "blocked" : "open";
|
|
1833
|
+
if (input.blockedBy) {
|
|
1834
|
+
const blocker = await resolveTask(client, input.blockedBy);
|
|
1835
|
+
blockedById = String(blocker.id);
|
|
1432
1836
|
}
|
|
1433
|
-
|
|
1434
|
-
let
|
|
1435
|
-
|
|
1436
|
-
const
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1837
|
+
let parentTaskId = null;
|
|
1838
|
+
let parentRef = input.parentTaskId;
|
|
1839
|
+
if (!parentRef) {
|
|
1840
|
+
const extracted = extractParentFromContext(input.context);
|
|
1841
|
+
if (extracted) {
|
|
1842
|
+
parentRef = extracted;
|
|
1843
|
+
process.stderr.write(
|
|
1844
|
+
"[create_task] auto-populated parent_task_id from context body \u2014 dispatchers should pass parent_task_id explicitly\n"
|
|
1845
|
+
);
|
|
1440
1846
|
}
|
|
1441
|
-
} catch {
|
|
1442
1847
|
}
|
|
1443
|
-
|
|
1444
|
-
const claudeJsonPath = path9.join(os5.homedir(), ".claude.json");
|
|
1445
|
-
let claudeJson = {};
|
|
1848
|
+
if (parentRef) {
|
|
1446
1849
|
try {
|
|
1447
|
-
|
|
1448
|
-
|
|
1850
|
+
const parent = await resolveTask(client, parentRef);
|
|
1851
|
+
parentTaskId = String(parent.id);
|
|
1852
|
+
} catch (err) {
|
|
1853
|
+
if (!input.parentTaskId) {
|
|
1854
|
+
throw new Error(
|
|
1855
|
+
`create_task: parent reference "${parentRef}" in context body does not resolve to an existing task`
|
|
1856
|
+
);
|
|
1857
|
+
}
|
|
1858
|
+
throw err;
|
|
1449
1859
|
}
|
|
1450
|
-
if (!claudeJson.projects) claudeJson.projects = {};
|
|
1451
|
-
const projects = claudeJson.projects;
|
|
1452
|
-
const trustDir = opts?.cwd ?? projectDir;
|
|
1453
|
-
if (!projects[trustDir]) projects[trustDir] = {};
|
|
1454
|
-
projects[trustDir].hasTrustDialogAccepted = true;
|
|
1455
|
-
writeFileSync4(claudeJsonPath, JSON.stringify(claudeJson, null, 2) + "\n");
|
|
1456
|
-
} catch {
|
|
1457
1860
|
}
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1861
|
+
let warning;
|
|
1862
|
+
const dupCheck = await client.execute({
|
|
1863
|
+
sql: "SELECT id FROM tasks WHERE title = ? AND assigned_to = ? AND status IN ('open', 'in_progress', 'blocked')",
|
|
1864
|
+
args: [input.title, input.assignedTo]
|
|
1865
|
+
});
|
|
1866
|
+
if (dupCheck.rows.length > 0) {
|
|
1867
|
+
warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
|
|
1868
|
+
}
|
|
1869
|
+
if (input.baseDir) {
|
|
1464
1870
|
try {
|
|
1465
|
-
|
|
1871
|
+
await mkdir3(path9.join(input.baseDir, "exe", "output"), { recursive: true });
|
|
1872
|
+
await mkdir3(path9.join(input.baseDir, "exe", "research"), { recursive: true });
|
|
1873
|
+
await ensureArchitectureDoc(input.baseDir, input.projectName);
|
|
1874
|
+
await ensureGitignoreExe(input.baseDir);
|
|
1466
1875
|
} catch {
|
|
1467
1876
|
}
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
"update_task",
|
|
1475
|
-
"list_tasks",
|
|
1476
|
-
"get_task",
|
|
1477
|
-
"ask_team_memory",
|
|
1478
|
-
"store_behavior",
|
|
1479
|
-
"get_identity",
|
|
1480
|
-
"send_message"
|
|
1481
|
-
];
|
|
1482
|
-
const requiredTools = expandDualPrefixTools(toolNames);
|
|
1483
|
-
let changed = false;
|
|
1484
|
-
for (const tool of requiredTools) {
|
|
1485
|
-
if (!allow.includes(tool)) {
|
|
1486
|
-
allow.push(tool);
|
|
1487
|
-
changed = true;
|
|
1488
|
-
}
|
|
1489
|
-
}
|
|
1490
|
-
if (changed) {
|
|
1491
|
-
perms.allow = allow;
|
|
1492
|
-
settings.permissions = perms;
|
|
1493
|
-
mkdirSync4(projSettingsDir, { recursive: true });
|
|
1494
|
-
writeFileSync4(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
1495
|
-
}
|
|
1877
|
+
}
|
|
1878
|
+
const complexity = input.complexity ?? "standard";
|
|
1879
|
+
let sessionScope = null;
|
|
1880
|
+
try {
|
|
1881
|
+
const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
|
|
1882
|
+
sessionScope = resolveExeSession2();
|
|
1496
1883
|
} catch {
|
|
1497
1884
|
}
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1885
|
+
await client.execute({
|
|
1886
|
+
sql: `INSERT INTO tasks (id, title, assigned_to, assigned_by, project_name, priority, status, task_file, blocked_by, parent_task_id, reviewer, context, complexity, budget_tokens, budget_fallback_model, tokens_used, tokens_warned_at, session_scope, created_at, updated_at)
|
|
1887
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
1888
|
+
args: [
|
|
1889
|
+
id,
|
|
1890
|
+
input.title,
|
|
1891
|
+
input.assignedTo,
|
|
1892
|
+
input.assignedBy,
|
|
1893
|
+
input.projectName,
|
|
1894
|
+
input.priority,
|
|
1895
|
+
initialStatus,
|
|
1896
|
+
taskFile,
|
|
1897
|
+
blockedById,
|
|
1898
|
+
parentTaskId,
|
|
1899
|
+
input.reviewer ?? null,
|
|
1900
|
+
input.context,
|
|
1901
|
+
complexity,
|
|
1902
|
+
input.budgetTokens ?? null,
|
|
1903
|
+
input.budgetFallbackModel ?? null,
|
|
1904
|
+
0,
|
|
1905
|
+
null,
|
|
1906
|
+
sessionScope,
|
|
1907
|
+
now,
|
|
1908
|
+
now
|
|
1909
|
+
]
|
|
1910
|
+
});
|
|
1911
|
+
return {
|
|
1912
|
+
id,
|
|
1913
|
+
title: input.title,
|
|
1914
|
+
assignedTo: input.assignedTo,
|
|
1915
|
+
assignedBy: input.assignedBy,
|
|
1916
|
+
projectName: input.projectName,
|
|
1917
|
+
priority: input.priority,
|
|
1918
|
+
status: initialStatus,
|
|
1919
|
+
taskFile,
|
|
1920
|
+
createdAt: now,
|
|
1921
|
+
updatedAt: now,
|
|
1922
|
+
warning,
|
|
1923
|
+
budgetTokens: input.budgetTokens ?? null,
|
|
1924
|
+
budgetFallbackModel: input.budgetFallbackModel ?? null,
|
|
1925
|
+
tokensUsed: 0,
|
|
1926
|
+
tokensWarnedAt: null
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
async function listTasks(input) {
|
|
1930
|
+
const client = getClient();
|
|
1931
|
+
const conditions = [];
|
|
1932
|
+
const args = [];
|
|
1933
|
+
if (input.assignedTo) {
|
|
1934
|
+
conditions.push("assigned_to = ?");
|
|
1935
|
+
args.push(input.assignedTo);
|
|
1528
1936
|
}
|
|
1529
|
-
if (
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
);
|
|
1937
|
+
if (input.status) {
|
|
1938
|
+
conditions.push("status = ?");
|
|
1939
|
+
args.push(input.status);
|
|
1940
|
+
} else {
|
|
1941
|
+
conditions.push("status IN ('open', 'in_progress', 'blocked')");
|
|
1942
|
+
}
|
|
1943
|
+
if (input.projectName) {
|
|
1944
|
+
conditions.push("project_name = ?");
|
|
1945
|
+
args.push(input.projectName);
|
|
1946
|
+
}
|
|
1947
|
+
if (input.priority) {
|
|
1948
|
+
conditions.push("priority = ?");
|
|
1949
|
+
args.push(input.priority);
|
|
1534
1950
|
}
|
|
1535
|
-
let sessionContextFlag = "";
|
|
1536
1951
|
try {
|
|
1537
|
-
const
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
`Your parent exe session is ${exeSession}.`,
|
|
1544
|
-
`Your employees (if any) use the -${exeSession} suffix (e.g., tom-${exeSession}).`
|
|
1545
|
-
].join("\n");
|
|
1546
|
-
writeFileSync4(ctxFile, ctxContent);
|
|
1547
|
-
sessionContextFlag = ` --append-system-prompt-file ${ctxFile}`;
|
|
1952
|
+
const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
|
|
1953
|
+
const session = resolveExeSession2();
|
|
1954
|
+
if (session) {
|
|
1955
|
+
conditions.push("(session_scope IS NULL OR session_scope = ?)");
|
|
1956
|
+
args.push(session);
|
|
1957
|
+
}
|
|
1548
1958
|
} catch {
|
|
1549
1959
|
}
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1960
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1961
|
+
const result = await client.execute({
|
|
1962
|
+
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`,
|
|
1963
|
+
args
|
|
1964
|
+
});
|
|
1965
|
+
return result.rows.map((r) => ({
|
|
1966
|
+
id: String(r.id),
|
|
1967
|
+
title: String(r.title),
|
|
1968
|
+
assignedTo: String(r.assigned_to),
|
|
1969
|
+
assignedBy: String(r.assigned_by),
|
|
1970
|
+
projectName: String(r.project_name),
|
|
1971
|
+
priority: String(r.priority),
|
|
1972
|
+
status: String(r.status),
|
|
1973
|
+
taskFile: String(r.task_file),
|
|
1974
|
+
createdAt: String(r.created_at),
|
|
1975
|
+
updatedAt: String(r.updated_at),
|
|
1976
|
+
checkpointCount: Number(r.checkpoint_count ?? 0),
|
|
1977
|
+
budgetTokens: r.budget_tokens !== null ? Number(r.budget_tokens) : null,
|
|
1978
|
+
budgetFallbackModel: r.budget_fallback_model !== null ? String(r.budget_fallback_model) : null,
|
|
1979
|
+
tokensUsed: Number(r.tokens_used ?? 0),
|
|
1980
|
+
tokensWarnedAt: r.tokens_warned_at !== null ? Number(r.tokens_warned_at) : null
|
|
1981
|
+
}));
|
|
1982
|
+
}
|
|
1983
|
+
function checkStaleCompletion(taskContext, taskCreatedAt) {
|
|
1984
|
+
if (!taskContext) return null;
|
|
1985
|
+
if (!DELEGATION_KEYWORDS.test(taskContext)) return null;
|
|
1986
|
+
try {
|
|
1987
|
+
const since = new Date(taskCreatedAt).toISOString();
|
|
1988
|
+
const branch = execSync5(
|
|
1989
|
+
"git rev-parse --abbrev-ref HEAD 2>/dev/null",
|
|
1990
|
+
{ encoding: "utf8", timeout: 3e3 }
|
|
1991
|
+
).trim();
|
|
1992
|
+
const branchArg = branch && branch !== "HEAD" ? branch : "";
|
|
1993
|
+
const commitCount = execSync5(
|
|
1994
|
+
`git log --oneline --since="${since}" ${branchArg} 2>/dev/null | wc -l`,
|
|
1995
|
+
{ encoding: "utf8", timeout: 5e3 }
|
|
1996
|
+
).trim();
|
|
1997
|
+
const count = parseInt(commitCount, 10);
|
|
1998
|
+
if (count === 0) {
|
|
1999
|
+
return "WARNING: task closed with no new commits since creation. Verify work was actually produced.";
|
|
1558
2000
|
}
|
|
2001
|
+
return null;
|
|
2002
|
+
} catch {
|
|
2003
|
+
return null;
|
|
1559
2004
|
}
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
2005
|
+
}
|
|
2006
|
+
async function updateTaskStatus(input) {
|
|
2007
|
+
const client = getClient();
|
|
2008
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2009
|
+
const row = await resolveTask(client, input.taskId);
|
|
2010
|
+
const taskId = String(row.id);
|
|
2011
|
+
const taskFile = String(row.task_file);
|
|
2012
|
+
if (input.status === "done" && String(row.assigned_by) === "system" && taskFile.includes("review-")) {
|
|
1565
2013
|
process.stderr.write(
|
|
1566
|
-
`[
|
|
2014
|
+
`[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
|
|
1567
2015
|
`
|
|
1568
2016
|
);
|
|
1569
|
-
spawnCommand = `${envPrefix} ${binName}${cleanupSuffix}`;
|
|
1570
|
-
} else {
|
|
1571
|
-
spawnCommand = `${envPrefix} claude --dangerously-skip-permissions${identityFlag}${behaviorsFlag}${sessionContextFlag}${cleanupSuffix}`;
|
|
1572
|
-
}
|
|
1573
|
-
const spawnResult = transport.spawn(sessionName, {
|
|
1574
|
-
cwd: spawnCwd,
|
|
1575
|
-
command: spawnCommand
|
|
1576
|
-
});
|
|
1577
|
-
if (spawnResult.error) {
|
|
1578
|
-
releaseSpawnLock(sessionName);
|
|
1579
|
-
return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
|
|
1580
2017
|
}
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
2018
|
+
if (input.status === "done") {
|
|
2019
|
+
const existingRow = await client.execute({
|
|
2020
|
+
sql: "SELECT context, created_at FROM tasks WHERE id = ?",
|
|
2021
|
+
args: [taskId]
|
|
2022
|
+
});
|
|
2023
|
+
if (existingRow.rows.length > 0) {
|
|
2024
|
+
const ctx = existingRow.rows[0];
|
|
2025
|
+
const warning = checkStaleCompletion(ctx.context, ctx.created_at);
|
|
2026
|
+
if (warning) {
|
|
2027
|
+
input.result = input.result ? `\u26A0\uFE0F ${warning}
|
|
2028
|
+
|
|
2029
|
+
${input.result}` : `\u26A0\uFE0F ${warning}`;
|
|
2030
|
+
process.stderr.write(`[tasks] ${warning} (task: ${taskId})
|
|
2031
|
+
`);
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
1592
2034
|
}
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
2035
|
+
if (input.status === "in_progress") {
|
|
2036
|
+
const tmuxSession = process.env.TMUX_PANE ?? process.env.MY_TMUX_SESSION ?? "unknown";
|
|
2037
|
+
const claim = await client.execute({
|
|
2038
|
+
sql: `UPDATE tasks
|
|
2039
|
+
SET status = 'in_progress', assigned_tmux = ?, updated_at = ?
|
|
2040
|
+
WHERE id = ? AND status = 'open'`,
|
|
2041
|
+
args: [tmuxSession, now, taskId]
|
|
2042
|
+
});
|
|
2043
|
+
if (claim.rowsAffected === 0) {
|
|
2044
|
+
const current = await client.execute({
|
|
2045
|
+
sql: "SELECT status, assigned_tmux FROM tasks WHERE id = ?",
|
|
2046
|
+
args: [taskId]
|
|
2047
|
+
});
|
|
2048
|
+
const cur = current.rows[0];
|
|
2049
|
+
const status = cur?.status ?? "unknown";
|
|
2050
|
+
const claimedBy = cur?.assigned_tmux ? ` (claimed by ${cur.assigned_tmux})` : "";
|
|
2051
|
+
throw new Error(`${TASK_ALREADY_CLAIMED_PREFIX}: task ${taskId} is ${status}${claimedBy}`);
|
|
1598
2052
|
}
|
|
1599
2053
|
try {
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
}
|
|
1606
|
-
} else {
|
|
1607
|
-
if (pane.includes("Claude Code") || pane.includes("\u276F")) {
|
|
1608
|
-
booted = true;
|
|
1609
|
-
break;
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
2054
|
+
await writeCheckpoint({
|
|
2055
|
+
taskId,
|
|
2056
|
+
step: "claimed",
|
|
2057
|
+
contextSummary: `Task claimed by session. Transitioning open \u2192 in_progress.`
|
|
2058
|
+
});
|
|
1612
2059
|
} catch {
|
|
1613
2060
|
}
|
|
2061
|
+
return { row, taskFile, now, taskId };
|
|
1614
2062
|
}
|
|
1615
|
-
if (
|
|
1616
|
-
|
|
1617
|
-
|
|
2063
|
+
if (input.result) {
|
|
2064
|
+
await client.execute({
|
|
2065
|
+
sql: "UPDATE tasks SET status = ?, result = ?, updated_at = ? WHERE id = ?",
|
|
2066
|
+
args: [input.status, input.result, now, taskId]
|
|
2067
|
+
});
|
|
2068
|
+
} else {
|
|
2069
|
+
await client.execute({
|
|
2070
|
+
sql: "UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?",
|
|
2071
|
+
args: [input.status, now, taskId]
|
|
2072
|
+
});
|
|
1618
2073
|
}
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
2074
|
+
try {
|
|
2075
|
+
await writeCheckpoint({
|
|
2076
|
+
taskId,
|
|
2077
|
+
step: `status_transition:${input.status}`,
|
|
2078
|
+
contextSummary: input.result ? `Transitioned to ${input.status}. Result: ${input.result.slice(0, 500)}` : `Transitioned to ${input.status}.`
|
|
2079
|
+
});
|
|
2080
|
+
} catch {
|
|
2081
|
+
}
|
|
2082
|
+
return { row, taskFile, now, taskId };
|
|
2083
|
+
}
|
|
2084
|
+
async function deleteTaskCore(taskId, _baseDir) {
|
|
2085
|
+
const client = getClient();
|
|
2086
|
+
const row = await resolveTask(client, taskId);
|
|
2087
|
+
const id = String(row.id);
|
|
2088
|
+
const taskFile = String(row.task_file);
|
|
2089
|
+
const assignedTo = String(row.assigned_to);
|
|
2090
|
+
const assignedBy = String(row.assigned_by);
|
|
2091
|
+
await client.execute({ sql: "DELETE FROM tasks WHERE id = ?", args: [id] });
|
|
2092
|
+
const taskSlug = taskFile.split("/").pop()?.replace(".md", "") ?? "";
|
|
2093
|
+
return { taskFile, assignedTo, assignedBy, taskSlug };
|
|
2094
|
+
}
|
|
2095
|
+
async function ensureArchitectureDoc(baseDir, projectName) {
|
|
2096
|
+
const archPath = path9.join(baseDir, "exe", "ARCHITECTURE.md");
|
|
2097
|
+
try {
|
|
2098
|
+
if (existsSync9(archPath)) return;
|
|
2099
|
+
const template = [
|
|
2100
|
+
`# ${projectName} \u2014 System Architecture`,
|
|
2101
|
+
"",
|
|
2102
|
+
"> Employees: read this before every task. Update it when you change system structure.",
|
|
2103
|
+
`> Last updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
|
|
2104
|
+
"",
|
|
2105
|
+
"## Overview",
|
|
2106
|
+
"",
|
|
2107
|
+
"<!-- Describe what this system does, its main components, and how they connect. -->",
|
|
2108
|
+
"",
|
|
2109
|
+
"## Key Components",
|
|
2110
|
+
"",
|
|
2111
|
+
"<!-- List the major modules, services, or subsystems. -->",
|
|
2112
|
+
"",
|
|
2113
|
+
"## Data Flow",
|
|
2114
|
+
"",
|
|
2115
|
+
"<!-- How does data move through the system? What writes where? -->",
|
|
2116
|
+
"",
|
|
2117
|
+
"## Invariants",
|
|
2118
|
+
"",
|
|
2119
|
+
"<!-- Rules that must never be violated. What breaks if these are wrong? -->",
|
|
2120
|
+
"",
|
|
2121
|
+
"## Dependencies",
|
|
2122
|
+
"",
|
|
2123
|
+
"<!-- What depends on what? If I change X, what else is affected? -->",
|
|
2124
|
+
""
|
|
2125
|
+
].join("\n");
|
|
2126
|
+
await writeFile3(archPath, template, "utf-8");
|
|
2127
|
+
} catch {
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
async function ensureGitignoreExe(baseDir) {
|
|
2131
|
+
const gitignorePath = path9.join(baseDir, ".gitignore");
|
|
2132
|
+
try {
|
|
2133
|
+
if (existsSync9(gitignorePath)) {
|
|
2134
|
+
const content = readFileSync9(gitignorePath, "utf-8");
|
|
2135
|
+
if (/^\/?exe\/?$/m.test(content)) return;
|
|
2136
|
+
await appendFile(gitignorePath, "\n# Employee task assignments (private)\n/exe/\n");
|
|
2137
|
+
} else {
|
|
2138
|
+
await writeFile3(gitignorePath, "# Employee task assignments (private)\n/exe/\n", "utf-8");
|
|
1623
2139
|
}
|
|
2140
|
+
} catch {
|
|
1624
2141
|
}
|
|
1625
|
-
registerSession({
|
|
1626
|
-
windowName: sessionName,
|
|
1627
|
-
agentId: employeeName,
|
|
1628
|
-
projectDir: spawnCwd,
|
|
1629
|
-
parentExe: exeSession,
|
|
1630
|
-
pid: 0,
|
|
1631
|
-
registeredAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1632
|
-
});
|
|
1633
|
-
releaseSpawnLock(sessionName);
|
|
1634
|
-
return { sessionName };
|
|
1635
2142
|
}
|
|
1636
|
-
var
|
|
1637
|
-
var
|
|
1638
|
-
"src/lib/
|
|
2143
|
+
var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
|
|
2144
|
+
var init_tasks_crud = __esm({
|
|
2145
|
+
"src/lib/tasks-crud.ts"() {
|
|
1639
2146
|
"use strict";
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
init_cc_agent_support();
|
|
1644
|
-
init_mcp_prefix();
|
|
1645
|
-
init_provider_table();
|
|
1646
|
-
init_intercom_queue();
|
|
1647
|
-
init_plan_limits();
|
|
1648
|
-
SPAWN_LOCK_DIR = path9.join(os5.homedir(), ".exe-os", "spawn-locks");
|
|
1649
|
-
SESSION_CACHE = path9.join(os5.homedir(), ".exe-os", "session-cache");
|
|
1650
|
-
BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
|
|
1651
|
-
INTERCOM_DEBOUNCE_MS = 3e4;
|
|
1652
|
-
INTERCOM_LOG2 = path9.join(os5.homedir(), ".exe-os", "intercom.log");
|
|
1653
|
-
DEBOUNCE_FILE = path9.join(SESSION_CACHE, "intercom-debounce.json");
|
|
1654
|
-
DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
|
|
1655
|
-
BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
|
|
2147
|
+
init_database();
|
|
2148
|
+
DELEGATION_KEYWORDS = /parallel|delegate|wave|tom\d*-exe/i;
|
|
2149
|
+
TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
|
|
1656
2150
|
}
|
|
1657
2151
|
});
|
|
1658
2152
|
|
|
@@ -1827,6 +2321,7 @@ var init_tasks_review = __esm({
|
|
|
1827
2321
|
init_tasks_crud();
|
|
1828
2322
|
init_tmux_routing();
|
|
1829
2323
|
init_session_key();
|
|
2324
|
+
init_state_bus();
|
|
1830
2325
|
}
|
|
1831
2326
|
});
|
|
1832
2327
|
|
|
@@ -1991,13 +2486,12 @@ function assertSessionScope(actionType, targetProject) {
|
|
|
1991
2486
|
};
|
|
1992
2487
|
}
|
|
1993
2488
|
process.stderr.write(
|
|
1994
|
-
`[session-scope]
|
|
2489
|
+
`[session-scope] BLOCKED cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
|
|
1995
2490
|
`
|
|
1996
2491
|
);
|
|
1997
2492
|
return {
|
|
1998
|
-
allowed:
|
|
1999
|
-
|
|
2000
|
-
reason: "cross_session_granted",
|
|
2493
|
+
allowed: false,
|
|
2494
|
+
reason: "cross_session_denied",
|
|
2001
2495
|
currentProject,
|
|
2002
2496
|
targetProject,
|
|
2003
2497
|
targetSession: findSessionForProject(targetProject)?.windowName
|
|
@@ -2023,8 +2517,9 @@ async function dispatchTaskToEmployee(input) {
|
|
|
2023
2517
|
try {
|
|
2024
2518
|
const { assertSessionScope: assertSessionScope2 } = (init_session_scope(), __toCommonJS(session_scope_exports));
|
|
2025
2519
|
const check = assertSessionScope2("dispatch_task", input.projectName);
|
|
2026
|
-
if (check.reason === "
|
|
2520
|
+
if (check.reason === "cross_session_denied") {
|
|
2027
2521
|
crossProject = true;
|
|
2522
|
+
return { dispatched: "skipped", crossProject: true };
|
|
2028
2523
|
}
|
|
2029
2524
|
} catch {
|
|
2030
2525
|
}
|
|
@@ -2081,10 +2576,10 @@ var init_tasks_notify = __esm({
|
|
|
2081
2576
|
});
|
|
2082
2577
|
|
|
2083
2578
|
// src/lib/behaviors.ts
|
|
2084
|
-
import
|
|
2579
|
+
import crypto4 from "crypto";
|
|
2085
2580
|
async function storeBehavior(opts) {
|
|
2086
2581
|
const client = getClient();
|
|
2087
|
-
const id =
|
|
2582
|
+
const id = crypto4.randomUUID();
|
|
2088
2583
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2089
2584
|
await client.execute({
|
|
2090
2585
|
sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
|
|
@@ -2113,7 +2608,7 @@ __export(skill_learning_exports, {
|
|
|
2113
2608
|
storeTrajectory: () => storeTrajectory,
|
|
2114
2609
|
sweepTrajectories: () => sweepTrajectories
|
|
2115
2610
|
});
|
|
2116
|
-
import
|
|
2611
|
+
import crypto5 from "crypto";
|
|
2117
2612
|
async function extractTrajectory(taskId, agentId) {
|
|
2118
2613
|
const client = getClient();
|
|
2119
2614
|
const result = await client.execute({
|
|
@@ -2142,11 +2637,11 @@ async function extractTrajectory(taskId, agentId) {
|
|
|
2142
2637
|
return signature;
|
|
2143
2638
|
}
|
|
2144
2639
|
function hashSignature(signature) {
|
|
2145
|
-
return
|
|
2640
|
+
return crypto5.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
|
|
2146
2641
|
}
|
|
2147
2642
|
async function storeTrajectory(opts) {
|
|
2148
2643
|
const client = getClient();
|
|
2149
|
-
const id =
|
|
2644
|
+
const id = crypto5.randomUUID();
|
|
2150
2645
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2151
2646
|
const signatureHash = hashSignature(opts.signature);
|
|
2152
2647
|
await client.execute({
|
|
@@ -2391,6 +2886,26 @@ var init_skill_learning = __esm({
|
|
|
2391
2886
|
});
|
|
2392
2887
|
|
|
2393
2888
|
// src/lib/tasks.ts
|
|
2889
|
+
var tasks_exports = {};
|
|
2890
|
+
__export(tasks_exports, {
|
|
2891
|
+
cleanupOrphanedReviews: () => cleanupOrphanedReviews,
|
|
2892
|
+
countNewPendingReviewsSince: () => countNewPendingReviewsSince,
|
|
2893
|
+
countPendingReviews: () => countPendingReviews,
|
|
2894
|
+
createTask: () => createTask,
|
|
2895
|
+
createTaskCore: () => createTaskCore,
|
|
2896
|
+
deleteTask: () => deleteTask,
|
|
2897
|
+
deleteTaskCore: () => deleteTaskCore,
|
|
2898
|
+
ensureArchitectureDoc: () => ensureArchitectureDoc,
|
|
2899
|
+
ensureGitignoreExe: () => ensureGitignoreExe,
|
|
2900
|
+
getReviewChecklist: () => getReviewChecklist,
|
|
2901
|
+
listPendingReviews: () => listPendingReviews,
|
|
2902
|
+
listTasks: () => listTasks,
|
|
2903
|
+
resolveTask: () => resolveTask,
|
|
2904
|
+
slugify: () => slugify,
|
|
2905
|
+
updateTask: () => updateTask,
|
|
2906
|
+
updateTaskStatus: () => updateTaskStatus,
|
|
2907
|
+
writeCheckpoint: () => writeCheckpoint
|
|
2908
|
+
});
|
|
2394
2909
|
import path13 from "path";
|
|
2395
2910
|
import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, unlinkSync as unlinkSync4 } from "fs";
|
|
2396
2911
|
async function createTask(input) {
|
|
@@ -2467,6 +2982,13 @@ async function updateTask(input) {
|
|
|
2467
2982
|
await cascadeUnblock(taskId, input.baseDir, now);
|
|
2468
2983
|
} catch {
|
|
2469
2984
|
}
|
|
2985
|
+
orgBus.emit({
|
|
2986
|
+
type: "task_completed",
|
|
2987
|
+
taskId,
|
|
2988
|
+
employee: String(row.assigned_to),
|
|
2989
|
+
result: input.result ?? "",
|
|
2990
|
+
timestamp: now
|
|
2991
|
+
});
|
|
2470
2992
|
if (row.parent_task_id) {
|
|
2471
2993
|
try {
|
|
2472
2994
|
await checkSubtaskCompletion(String(row.parent_task_id), String(row.project_name));
|
|
@@ -2533,6 +3055,7 @@ var init_tasks = __esm({
|
|
|
2533
3055
|
init_database();
|
|
2534
3056
|
init_config();
|
|
2535
3057
|
init_notifications();
|
|
3058
|
+
init_state_bus();
|
|
2536
3059
|
init_tasks_crud();
|
|
2537
3060
|
init_tasks_review();
|
|
2538
3061
|
init_tasks_crud();
|