@askexenow/exe-os 0.8.40 → 0.8.42
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 +1376 -659
- package/dist/bin/exe-agent.js +20 -1
- package/dist/bin/exe-assign.js +1503 -1343
- package/dist/bin/exe-boot.js +2549 -1784
- package/dist/bin/exe-call.js +39 -1
- package/dist/bin/exe-cloud.js +12 -2
- package/dist/bin/exe-dispatch.js +39 -2
- package/dist/bin/exe-doctor.js +791 -634
- package/dist/bin/exe-export-behaviors.js +792 -637
- package/dist/bin/exe-forget.js +145 -0
- package/dist/bin/exe-gateway.js +2501 -1846
- 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 +44 -12
- package/dist/bin/exe-new-employee.js +6 -2
- 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 +176 -1
- package/dist/bin/exe-rename.js +23 -0
- package/dist/bin/exe-review.js +490 -327
- package/dist/bin/exe-search.js +157 -4
- package/dist/bin/exe-session-cleanup.js +2487 -403
- package/dist/bin/exe-settings.js +2 -1
- package/dist/bin/exe-status.js +474 -317
- package/dist/bin/exe-team.js +474 -317
- package/dist/bin/git-sweep.js +2691 -151
- 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 +50 -26
- package/dist/bin/shard-migrate.js +792 -637
- package/dist/bin/wiki-sync.js +794 -637
- package/dist/gateway/index.js +2542 -1887
- package/dist/hooks/bug-report-worker.js +2118 -576
- package/dist/hooks/commit-complete.js +2690 -150
- package/dist/hooks/error-recall.js +157 -4
- package/dist/hooks/ingest-worker.js +1455 -803
- 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 +1670 -1509
- package/dist/hooks/prompt-submit.js +2650 -1074
- package/dist/hooks/response-ingest-worker.js +154 -6
- package/dist/hooks/session-end.js +153 -2
- package/dist/hooks/session-start.js +157 -4
- package/dist/hooks/stop.js +151 -0
- package/dist/hooks/subagent-stop.js +155 -2
- package/dist/hooks/summary-worker.js +190 -21
- package/dist/index.js +326 -102
- package/dist/lib/cloud-sync.js +31 -10
- package/dist/lib/config.js +2 -0
- 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/embedder.js +3 -1
- package/dist/lib/employee-templates.js +20 -1
- package/dist/lib/exe-daemon.js +285 -18
- package/dist/lib/hybrid-search.js +157 -4
- 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 +1485 -918
- package/dist/lib/tmux-routing.js +194 -10
- package/dist/mcp/server.js +1643 -924
- package/dist/mcp/tools/create-task.js +2283 -829
- 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 +79 -0
- package/dist/runtime/index.js +280 -68
- package/dist/tui/App.js +1485 -645
- 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`,
|
|
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();
|
|
@@ -894,14 +516,14 @@ var init_transport = __esm({
|
|
|
894
516
|
});
|
|
895
517
|
|
|
896
518
|
// src/lib/cc-agent-support.ts
|
|
897
|
-
import { execSync as
|
|
519
|
+
import { execSync as execSync2 } from "child_process";
|
|
898
520
|
function _resetCcAgentSupportCache() {
|
|
899
521
|
_cachedSupport = null;
|
|
900
522
|
}
|
|
901
523
|
function claudeSupportsAgentFlag() {
|
|
902
524
|
if (_cachedSupport !== null) return _cachedSupport;
|
|
903
525
|
try {
|
|
904
|
-
const helpOutput =
|
|
526
|
+
const helpOutput = execSync2("claude --help 2>&1", {
|
|
905
527
|
encoding: "utf-8",
|
|
906
528
|
timeout: 5e3
|
|
907
529
|
});
|
|
@@ -967,17 +589,17 @@ var init_provider_table = __esm({
|
|
|
967
589
|
});
|
|
968
590
|
|
|
969
591
|
// src/lib/intercom-queue.ts
|
|
970
|
-
import { readFileSync as
|
|
971
|
-
import
|
|
592
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, renameSync as renameSync2, existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
|
|
593
|
+
import path4 from "path";
|
|
972
594
|
import os4 from "os";
|
|
973
595
|
function ensureDir() {
|
|
974
|
-
const dir =
|
|
975
|
-
if (!
|
|
596
|
+
const dir = path4.dirname(QUEUE_PATH);
|
|
597
|
+
if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
|
|
976
598
|
}
|
|
977
599
|
function readQueue() {
|
|
978
600
|
try {
|
|
979
|
-
if (!
|
|
980
|
-
return JSON.parse(
|
|
601
|
+
if (!existsSync4(QUEUE_PATH)) return [];
|
|
602
|
+
return JSON.parse(readFileSync4(QUEUE_PATH, "utf8"));
|
|
981
603
|
} catch {
|
|
982
604
|
return [];
|
|
983
605
|
}
|
|
@@ -1009,25 +631,57 @@ var QUEUE_PATH, TTL_MS, INTERCOM_LOG;
|
|
|
1009
631
|
var init_intercom_queue = __esm({
|
|
1010
632
|
"src/lib/intercom-queue.ts"() {
|
|
1011
633
|
"use strict";
|
|
1012
|
-
QUEUE_PATH =
|
|
634
|
+
QUEUE_PATH = path4.join(os4.homedir(), ".exe-os", "intercom-queue.json");
|
|
1013
635
|
TTL_MS = 60 * 60 * 1e3;
|
|
1014
|
-
INTERCOM_LOG =
|
|
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"]);
|
|
1015
669
|
}
|
|
1016
670
|
});
|
|
1017
671
|
|
|
1018
672
|
// src/lib/license.ts
|
|
1019
|
-
import { readFileSync as
|
|
673
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync3, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
|
|
1020
674
|
import { randomUUID } from "crypto";
|
|
1021
|
-
import
|
|
675
|
+
import path6 from "path";
|
|
1022
676
|
import { jwtVerify, importSPKI } from "jose";
|
|
1023
677
|
var LICENSE_PATH, CACHE_PATH, DEVICE_ID_PATH, PLAN_LIMITS;
|
|
1024
678
|
var init_license = __esm({
|
|
1025
679
|
"src/lib/license.ts"() {
|
|
1026
680
|
"use strict";
|
|
1027
681
|
init_config();
|
|
1028
|
-
LICENSE_PATH =
|
|
1029
|
-
CACHE_PATH =
|
|
1030
|
-
DEVICE_ID_PATH =
|
|
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");
|
|
1031
685
|
PLAN_LIMITS = {
|
|
1032
686
|
free: { devices: 1, employees: 1, memories: 5e3 },
|
|
1033
687
|
pro: { devices: 2, employees: 5, memories: 1e5 },
|
|
@@ -1039,12 +693,12 @@ var init_license = __esm({
|
|
|
1039
693
|
});
|
|
1040
694
|
|
|
1041
695
|
// src/lib/plan-limits.ts
|
|
1042
|
-
import { readFileSync as
|
|
1043
|
-
import
|
|
696
|
+
import { readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
|
|
697
|
+
import path7 from "path";
|
|
1044
698
|
function getLicenseSync() {
|
|
1045
699
|
try {
|
|
1046
|
-
if (!
|
|
1047
|
-
const raw = JSON.parse(
|
|
700
|
+
if (!existsSync7(CACHE_PATH2)) return freeLicense();
|
|
701
|
+
const raw = JSON.parse(readFileSync7(CACHE_PATH2, "utf8"));
|
|
1048
702
|
if (!raw.token || typeof raw.token !== "string") return freeLicense();
|
|
1049
703
|
const parts = raw.token.split(".");
|
|
1050
704
|
if (parts.length !== 3) return freeLicense();
|
|
@@ -1082,8 +736,8 @@ function assertEmployeeLimitSync(rosterPath) {
|
|
|
1082
736
|
const filePath = rosterPath ?? EMPLOYEES_PATH;
|
|
1083
737
|
let count = 0;
|
|
1084
738
|
try {
|
|
1085
|
-
if (
|
|
1086
|
-
const raw =
|
|
739
|
+
if (existsSync7(filePath)) {
|
|
740
|
+
const raw = readFileSync7(filePath, "utf8");
|
|
1087
741
|
const employees = JSON.parse(raw);
|
|
1088
742
|
count = Array.isArray(employees) ? employees.length : 0;
|
|
1089
743
|
}
|
|
@@ -1098,33 +752,370 @@ function assertEmployeeLimitSync(rosterPath) {
|
|
|
1098
752
|
);
|
|
1099
753
|
}
|
|
1100
754
|
}
|
|
1101
|
-
var PlanLimitError, CACHE_PATH2;
|
|
1102
|
-
var init_plan_limits = __esm({
|
|
1103
|
-
"src/lib/plan-limits.ts"() {
|
|
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
|
+
);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
var init_session_kill_telemetry = __esm({
|
|
801
|
+
"src/lib/session-kill-telemetry.ts"() {
|
|
802
|
+
"use strict";
|
|
803
|
+
init_database();
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
|
|
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;
|
|
874
|
+
}
|
|
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]
|
|
921
|
+
});
|
|
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();
|
|
946
|
+
} catch {
|
|
947
|
+
return [];
|
|
948
|
+
}
|
|
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;
|
|
1041
|
+
}
|
|
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"() {
|
|
1104
1045
|
"use strict";
|
|
1046
|
+
init_session_registry();
|
|
1047
|
+
init_transport();
|
|
1048
|
+
init_notifications();
|
|
1105
1049
|
init_database();
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
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;
|
|
1116
1084
|
}
|
|
1117
1085
|
});
|
|
1118
1086
|
|
|
1119
1087
|
// src/lib/tmux-routing.ts
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
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";
|
|
1123
1114
|
import os5 from "os";
|
|
1124
1115
|
import { fileURLToPath } from "url";
|
|
1125
1116
|
import { unlinkSync as unlinkSync2 } from "fs";
|
|
1126
1117
|
function spawnLockPath(sessionName) {
|
|
1127
|
-
return
|
|
1118
|
+
return path8.join(SPAWN_LOCK_DIR, `${sessionName}.lock`);
|
|
1128
1119
|
}
|
|
1129
1120
|
function isProcessAlive(pid) {
|
|
1130
1121
|
try {
|
|
@@ -1135,13 +1126,13 @@ function isProcessAlive(pid) {
|
|
|
1135
1126
|
}
|
|
1136
1127
|
}
|
|
1137
1128
|
function acquireSpawnLock(sessionName) {
|
|
1138
|
-
if (!
|
|
1129
|
+
if (!existsSync8(SPAWN_LOCK_DIR)) {
|
|
1139
1130
|
mkdirSync4(SPAWN_LOCK_DIR, { recursive: true });
|
|
1140
1131
|
}
|
|
1141
1132
|
const lockFile = spawnLockPath(sessionName);
|
|
1142
|
-
if (
|
|
1133
|
+
if (existsSync8(lockFile)) {
|
|
1143
1134
|
try {
|
|
1144
|
-
const lock = JSON.parse(
|
|
1135
|
+
const lock = JSON.parse(readFileSync8(lockFile, "utf8"));
|
|
1145
1136
|
const age = Date.now() - lock.timestamp;
|
|
1146
1137
|
if (isProcessAlive(lock.pid) && age < 6e4) {
|
|
1147
1138
|
return false;
|
|
@@ -1149,510 +1140,1013 @@ function acquireSpawnLock(sessionName) {
|
|
|
1149
1140
|
} catch {
|
|
1150
1141
|
}
|
|
1151
1142
|
}
|
|
1152
|
-
writeFileSync4(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
|
|
1143
|
+
writeFileSync4(lockFile, JSON.stringify({ pid: process.pid, timestamp: Date.now() }));
|
|
1144
|
+
return true;
|
|
1145
|
+
}
|
|
1146
|
+
function releaseSpawnLock(sessionName) {
|
|
1147
|
+
try {
|
|
1148
|
+
unlinkSync2(spawnLockPath(sessionName));
|
|
1149
|
+
} catch {
|
|
1150
|
+
}
|
|
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 });
|
|
1224
|
+
}
|
|
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
|
+
}));
|
|
1232
|
+
}
|
|
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;
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
function getDispatchedBy(sessionKey) {
|
|
1242
|
+
try {
|
|
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;
|
|
1248
|
+
} catch {
|
|
1249
|
+
return null;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
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;
|
|
1264
|
+
}
|
|
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;
|
|
1274
|
+
}
|
|
1275
|
+
return null;
|
|
1276
|
+
}
|
|
1277
|
+
async function verifyPaneAtCapacity(sessionName) {
|
|
1278
|
+
const transport = getTransport();
|
|
1279
|
+
if (!transport.isAlive(sessionName)) {
|
|
1280
|
+
return { atCapacity: false, reason: `session ${sessionName} is not alive` };
|
|
1281
|
+
}
|
|
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)}`
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
const { isAtCapacity: isAtCapacity2 } = await Promise.resolve().then(() => (init_capacity_monitor(), capacity_monitor_exports));
|
|
1292
|
+
if (!isAtCapacity2(pane)) {
|
|
1293
|
+
return {
|
|
1294
|
+
atCapacity: false,
|
|
1295
|
+
reason: `last ${VERIFY_PANE_LINES} lines show normal work, no capacity banner`
|
|
1296
|
+
};
|
|
1297
|
+
}
|
|
1298
|
+
return {
|
|
1299
|
+
atCapacity: true,
|
|
1300
|
+
reason: "capacity banner matched in recent pane output"
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
function readDebounceState() {
|
|
1304
|
+
try {
|
|
1305
|
+
if (!existsSync8(DEBOUNCE_FILE)) return {};
|
|
1306
|
+
return JSON.parse(readFileSync8(DEBOUNCE_FILE, "utf8"));
|
|
1307
|
+
} catch {
|
|
1308
|
+
return {};
|
|
1309
|
+
}
|
|
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 {
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
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";
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
if (/Running…/.test(pane)) return "tool";
|
|
1353
|
+
if (BUSY_PATTERN.test(pane)) return "thinking";
|
|
1354
|
+
return "idle";
|
|
1355
|
+
} catch {
|
|
1356
|
+
return "offline";
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
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
|
+
}
|
|
1376
|
+
try {
|
|
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";
|
|
1403
|
+
} catch {
|
|
1404
|
+
logIntercom(`FAIL \u2192 ${targetSession}`);
|
|
1405
|
+
return "failed";
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
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;
|
|
1414
|
+
}
|
|
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";
|
|
1425
|
+
}
|
|
1426
|
+
return false;
|
|
1427
|
+
}
|
|
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
|
-
|
|
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 {
|
|
1329
1673
|
}
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
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 {
|
|
1335
1688
|
}
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1689
|
+
}
|
|
1690
|
+
if (!booted) {
|
|
1691
|
+
releaseSpawnLock(sessionName);
|
|
1692
|
+
return { sessionName, error: `${useExeAgent ? "exe-agent" : "claude"} did not boot within 15s` };
|
|
1693
|
+
}
|
|
1694
|
+
if (!useExeAgent) {
|
|
1695
|
+
try {
|
|
1696
|
+
transport.sendKeys(sessionName, `/exe-call ${employeeName}`);
|
|
1697
|
+
} catch {
|
|
1339
1698
|
}
|
|
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
1699
|
}
|
|
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 };
|
|
1348
1710
|
}
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
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…/;
|
|
1355
1733
|
}
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
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));
|
|
1368
1750
|
}
|
|
1369
|
-
|
|
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`);
|
|
1764
|
+
}
|
|
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 };
|
|
1370
1771
|
}
|
|
1371
|
-
function
|
|
1372
|
-
if (
|
|
1373
|
-
|
|
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.`
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
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
|
+
);
|
|
1822
|
+
}
|
|
1823
|
+
throw new Error(`Task not found: ${identifier}`);
|
|
1824
|
+
}
|
|
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);
|
|
1374
1836
|
}
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
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
|
+
);
|
|
1380
1846
|
}
|
|
1381
1847
|
}
|
|
1382
|
-
if (
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
employeeName,
|
|
1394
|
-
exeSession,
|
|
1395
|
-
opts.maxAutoInstances ?? 10
|
|
1396
|
-
);
|
|
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
|
-
};
|
|
1848
|
+
if (parentRef) {
|
|
1849
|
+
try {
|
|
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;
|
|
1403
1859
|
}
|
|
1404
|
-
effectiveInstance = free === 0 ? void 0 : free;
|
|
1405
1860
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
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) {
|
|
1870
|
+
try {
|
|
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);
|
|
1875
|
+
} catch {
|
|
1414
1876
|
}
|
|
1415
|
-
return { status: "failed", sessionName, error: "intercom delivery failed" };
|
|
1416
1877
|
}
|
|
1417
|
-
const
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
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();
|
|
1883
|
+
} catch {
|
|
1421
1884
|
}
|
|
1422
|
-
|
|
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
|
+
};
|
|
1423
1928
|
}
|
|
1424
|
-
function
|
|
1425
|
-
const
|
|
1426
|
-
const
|
|
1427
|
-
const
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
mkdirSync4(logDir, { recursive: true });
|
|
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);
|
|
1432
1936
|
}
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
if (existsSync9(cleanupScript)) {
|
|
1439
|
-
cleanupSuffix = `; ${process.execPath} "${cleanupScript}" "${employeeName}" "${exeSession}"`;
|
|
1440
|
-
}
|
|
1441
|
-
} catch {
|
|
1937
|
+
if (input.status) {
|
|
1938
|
+
conditions.push("status = ?");
|
|
1939
|
+
args.push(input.status);
|
|
1940
|
+
} else {
|
|
1941
|
+
conditions.push("status IN ('open', 'in_progress', 'blocked')");
|
|
1442
1942
|
}
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
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 {
|
|
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);
|
|
1457
1950
|
}
|
|
1458
1951
|
try {
|
|
1459
|
-
const
|
|
1460
|
-
const
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
try {
|
|
1465
|
-
settings = JSON.parse(readFileSync9(settingsPath, "utf8"));
|
|
1466
|
-
} catch {
|
|
1467
|
-
}
|
|
1468
|
-
const perms = settings.permissions ?? {};
|
|
1469
|
-
const allow = perms.allow ?? [];
|
|
1470
|
-
const toolNames = [
|
|
1471
|
-
"recall_my_memory",
|
|
1472
|
-
"store_memory",
|
|
1473
|
-
"create_task",
|
|
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");
|
|
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);
|
|
1495
1957
|
}
|
|
1496
1958
|
} catch {
|
|
1497
1959
|
}
|
|
1498
|
-
const
|
|
1499
|
-
const
|
|
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
|
-
|
|
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.";
|
|
1527
2000
|
}
|
|
2001
|
+
return null;
|
|
2002
|
+
} catch {
|
|
2003
|
+
return null;
|
|
1528
2004
|
}
|
|
1529
|
-
|
|
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-")) {
|
|
1530
2013
|
process.stderr.write(
|
|
1531
|
-
`[
|
|
2014
|
+
`[updateTask] Review task "${String(row.title)}" being marked done (assigned to ${String(row.assigned_to)})
|
|
1532
2015
|
`
|
|
1533
2016
|
);
|
|
1534
2017
|
}
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
}
|
|
1550
|
-
let envPrefix = `EXE_SESSION=${exeSession} EXE_SESSION_NAME=${sessionName}`;
|
|
1551
|
-
if (ccProvider !== DEFAULT_PROVIDER) {
|
|
1552
|
-
const cfg = PROVIDER_TABLE[ccProvider];
|
|
1553
|
-
if (cfg?.apiKeyEnv) {
|
|
1554
|
-
const keyVal = process.env[cfg.apiKeyEnv];
|
|
1555
|
-
if (keyVal) {
|
|
1556
|
-
envPrefix = `${envPrefix} ${cfg.apiKeyEnv}=${keyVal}`;
|
|
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
|
+
`);
|
|
1557
2032
|
}
|
|
1558
2033
|
}
|
|
1559
2034
|
}
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
)
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
if (spawnResult.error) {
|
|
1578
|
-
releaseSpawnLock(sessionName);
|
|
1579
|
-
return { sessionName, error: `tmux new-session failed: ${spawnResult.error}` };
|
|
1580
|
-
}
|
|
1581
|
-
transport.pipeLog(sessionName, logFile);
|
|
1582
|
-
try {
|
|
1583
|
-
const mySession = getMySession();
|
|
1584
|
-
const dispatchInfo = path9.join(SESSION_CACHE, `dispatch-info-${sessionName}.json`);
|
|
1585
|
-
writeFileSync4(dispatchInfo, JSON.stringify({
|
|
1586
|
-
dispatchedBy: mySession,
|
|
1587
|
-
rootExe: exeSession,
|
|
1588
|
-
provider: useBinSymlink ? ccProvider : useExeAgent ? opts.provider : "anthropic",
|
|
1589
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1590
|
-
}));
|
|
1591
|
-
} catch {
|
|
1592
|
-
}
|
|
1593
|
-
let booted = false;
|
|
1594
|
-
for (let i = 0; i < 30; i++) {
|
|
1595
|
-
try {
|
|
1596
|
-
execSync5("sleep 0.5");
|
|
1597
|
-
} catch {
|
|
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
|
|
|
@@ -1686,6 +2180,34 @@ async function listPendingReviews(limit) {
|
|
|
1686
2180
|
});
|
|
1687
2181
|
return result.rows;
|
|
1688
2182
|
}
|
|
2183
|
+
async function cleanupOrphanedReviews() {
|
|
2184
|
+
const client = getClient();
|
|
2185
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2186
|
+
const r1 = await client.execute({
|
|
2187
|
+
sql: `UPDATE tasks SET status = 'done', updated_at = ?
|
|
2188
|
+
WHERE status = 'needs_review'
|
|
2189
|
+
AND assigned_by = 'system'
|
|
2190
|
+
AND title LIKE 'Review:%'
|
|
2191
|
+
AND parent_task_id IN (SELECT id FROM tasks WHERE status IN ('done', 'cancelled'))`,
|
|
2192
|
+
args: [now]
|
|
2193
|
+
});
|
|
2194
|
+
const staleThreshold = new Date(Date.now() - 60 * 60 * 1e3).toISOString();
|
|
2195
|
+
const r2 = await client.execute({
|
|
2196
|
+
sql: `UPDATE tasks SET status = 'done', updated_at = ?
|
|
2197
|
+
WHERE status = 'needs_review'
|
|
2198
|
+
AND result IS NOT NULL
|
|
2199
|
+
AND updated_at < ?`,
|
|
2200
|
+
args: [now, staleThreshold]
|
|
2201
|
+
});
|
|
2202
|
+
const total = r1.rowsAffected + r2.rowsAffected;
|
|
2203
|
+
if (total > 0) {
|
|
2204
|
+
process.stderr.write(
|
|
2205
|
+
`[cleanup] Closed ${total} orphaned review(s): ${r1.rowsAffected} cascade + ${r2.rowsAffected} stale
|
|
2206
|
+
`
|
|
2207
|
+
);
|
|
2208
|
+
}
|
|
2209
|
+
return total;
|
|
2210
|
+
}
|
|
1689
2211
|
function getReviewChecklist(role, agent, taskSlug) {
|
|
1690
2212
|
const roleLower = role.toLowerCase();
|
|
1691
2213
|
if (roleLower.includes("engineer") || roleLower === "principal engineer") {
|
|
@@ -1799,6 +2321,7 @@ var init_tasks_review = __esm({
|
|
|
1799
2321
|
init_tasks_crud();
|
|
1800
2322
|
init_tmux_routing();
|
|
1801
2323
|
init_session_key();
|
|
2324
|
+
init_state_bus();
|
|
1802
2325
|
}
|
|
1803
2326
|
});
|
|
1804
2327
|
|
|
@@ -1963,13 +2486,12 @@ function assertSessionScope(actionType, targetProject) {
|
|
|
1963
2486
|
};
|
|
1964
2487
|
}
|
|
1965
2488
|
process.stderr.write(
|
|
1966
|
-
`[session-scope]
|
|
2489
|
+
`[session-scope] BLOCKED cross-project ${actionType}: session project="${currentProject}" \u2260 target project="${targetProject}"
|
|
1967
2490
|
`
|
|
1968
2491
|
);
|
|
1969
2492
|
return {
|
|
1970
|
-
allowed:
|
|
1971
|
-
|
|
1972
|
-
reason: "cross_session_granted",
|
|
2493
|
+
allowed: false,
|
|
2494
|
+
reason: "cross_session_denied",
|
|
1973
2495
|
currentProject,
|
|
1974
2496
|
targetProject,
|
|
1975
2497
|
targetSession: findSessionForProject(targetProject)?.windowName
|
|
@@ -1995,8 +2517,9 @@ async function dispatchTaskToEmployee(input) {
|
|
|
1995
2517
|
try {
|
|
1996
2518
|
const { assertSessionScope: assertSessionScope2 } = (init_session_scope(), __toCommonJS(session_scope_exports));
|
|
1997
2519
|
const check = assertSessionScope2("dispatch_task", input.projectName);
|
|
1998
|
-
if (check.reason === "
|
|
2520
|
+
if (check.reason === "cross_session_denied") {
|
|
1999
2521
|
crossProject = true;
|
|
2522
|
+
return { dispatched: "skipped", crossProject: true };
|
|
2000
2523
|
}
|
|
2001
2524
|
} catch {
|
|
2002
2525
|
}
|
|
@@ -2053,10 +2576,10 @@ var init_tasks_notify = __esm({
|
|
|
2053
2576
|
});
|
|
2054
2577
|
|
|
2055
2578
|
// src/lib/behaviors.ts
|
|
2056
|
-
import
|
|
2579
|
+
import crypto4 from "crypto";
|
|
2057
2580
|
async function storeBehavior(opts) {
|
|
2058
2581
|
const client = getClient();
|
|
2059
|
-
const id =
|
|
2582
|
+
const id = crypto4.randomUUID();
|
|
2060
2583
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2061
2584
|
await client.execute({
|
|
2062
2585
|
sql: `INSERT INTO behaviors (id, agent_id, project_name, domain, priority, content, active, created_at, updated_at)
|
|
@@ -2085,7 +2608,7 @@ __export(skill_learning_exports, {
|
|
|
2085
2608
|
storeTrajectory: () => storeTrajectory,
|
|
2086
2609
|
sweepTrajectories: () => sweepTrajectories
|
|
2087
2610
|
});
|
|
2088
|
-
import
|
|
2611
|
+
import crypto5 from "crypto";
|
|
2089
2612
|
async function extractTrajectory(taskId, agentId) {
|
|
2090
2613
|
const client = getClient();
|
|
2091
2614
|
const result = await client.execute({
|
|
@@ -2114,11 +2637,11 @@ async function extractTrajectory(taskId, agentId) {
|
|
|
2114
2637
|
return signature;
|
|
2115
2638
|
}
|
|
2116
2639
|
function hashSignature(signature) {
|
|
2117
|
-
return
|
|
2640
|
+
return crypto5.createHash("sha256").update(signature.join("|")).digest("hex").slice(0, 16);
|
|
2118
2641
|
}
|
|
2119
2642
|
async function storeTrajectory(opts) {
|
|
2120
2643
|
const client = getClient();
|
|
2121
|
-
const id =
|
|
2644
|
+
const id = crypto5.randomUUID();
|
|
2122
2645
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2123
2646
|
const signatureHash = hashSignature(opts.signature);
|
|
2124
2647
|
await client.execute({
|
|
@@ -2363,6 +2886,26 @@ var init_skill_learning = __esm({
|
|
|
2363
2886
|
});
|
|
2364
2887
|
|
|
2365
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
|
+
});
|
|
2366
2909
|
import path13 from "path";
|
|
2367
2910
|
import { writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, unlinkSync as unlinkSync4 } from "fs";
|
|
2368
2911
|
async function createTask(input) {
|
|
@@ -2411,6 +2954,21 @@ async function updateTask(input) {
|
|
|
2411
2954
|
});
|
|
2412
2955
|
} catch {
|
|
2413
2956
|
}
|
|
2957
|
+
try {
|
|
2958
|
+
const client = getClient();
|
|
2959
|
+
const cascaded = await client.execute({
|
|
2960
|
+
sql: `UPDATE tasks SET status = 'done', updated_at = ?
|
|
2961
|
+
WHERE parent_task_id = ? AND status = 'needs_review'`,
|
|
2962
|
+
args: [now, taskId]
|
|
2963
|
+
});
|
|
2964
|
+
if (cascaded.rowsAffected > 0) {
|
|
2965
|
+
process.stderr.write(
|
|
2966
|
+
`[cascade] Closed ${cascaded.rowsAffected} orphaned review task(s) for parent ${taskId}
|
|
2967
|
+
`
|
|
2968
|
+
);
|
|
2969
|
+
}
|
|
2970
|
+
} catch {
|
|
2971
|
+
}
|
|
2414
2972
|
}
|
|
2415
2973
|
const isTerminal = input.status === "done" || input.status === "needs_review";
|
|
2416
2974
|
if (isTerminal) {
|
|
@@ -2424,6 +2982,13 @@ async function updateTask(input) {
|
|
|
2424
2982
|
await cascadeUnblock(taskId, input.baseDir, now);
|
|
2425
2983
|
} catch {
|
|
2426
2984
|
}
|
|
2985
|
+
orgBus.emit({
|
|
2986
|
+
type: "task_completed",
|
|
2987
|
+
taskId,
|
|
2988
|
+
employee: String(row.assigned_to),
|
|
2989
|
+
result: input.result ?? "",
|
|
2990
|
+
timestamp: now
|
|
2991
|
+
});
|
|
2427
2992
|
if (row.parent_task_id) {
|
|
2428
2993
|
try {
|
|
2429
2994
|
await checkSubtaskCompletion(String(row.parent_task_id), String(row.project_name));
|
|
@@ -2490,6 +3055,7 @@ var init_tasks = __esm({
|
|
|
2490
3055
|
init_database();
|
|
2491
3056
|
init_config();
|
|
2492
3057
|
init_notifications();
|
|
3058
|
+
init_state_bus();
|
|
2493
3059
|
init_tasks_crud();
|
|
2494
3060
|
init_tasks_review();
|
|
2495
3061
|
init_tasks_crud();
|
|
@@ -2500,6 +3066,7 @@ var init_tasks = __esm({
|
|
|
2500
3066
|
});
|
|
2501
3067
|
init_tasks();
|
|
2502
3068
|
export {
|
|
3069
|
+
cleanupOrphanedReviews,
|
|
2503
3070
|
countNewPendingReviewsSince,
|
|
2504
3071
|
countPendingReviews,
|
|
2505
3072
|
createTask,
|