@askexenow/exe-os 0.8.83 → 0.8.85
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 +746 -595
- package/dist/bin/backfill-responses.js +745 -594
- package/dist/bin/backfill-vectors.js +312 -226
- package/dist/bin/cleanup-stale-review-tasks.js +97 -2
- package/dist/bin/cli.js +14350 -12518
- package/dist/bin/exe-agent.js +97 -88
- package/dist/bin/exe-assign.js +1003 -854
- package/dist/bin/exe-boot.js +1257 -320
- package/dist/bin/exe-call.js +10 -0
- package/dist/bin/exe-cloud.js +29 -6
- package/dist/bin/exe-dispatch.js +210 -34
- package/dist/bin/exe-doctor.js +403 -6
- package/dist/bin/exe-export-behaviors.js +175 -72
- package/dist/bin/exe-forget.js +97 -2
- package/dist/bin/exe-gateway.js +550 -171
- package/dist/bin/exe-healthcheck.js +1 -0
- package/dist/bin/exe-heartbeat.js +100 -5
- package/dist/bin/exe-kill.js +175 -72
- package/dist/bin/exe-launch-agent.js +189 -76
- package/dist/bin/exe-link.js +902 -80
- package/dist/bin/exe-new-employee.js +38 -8
- package/dist/bin/exe-pending-messages.js +96 -2
- package/dist/bin/exe-pending-notifications.js +97 -2
- package/dist/bin/exe-pending-reviews.js +98 -3
- package/dist/bin/exe-rename.js +564 -23
- package/dist/bin/exe-review.js +231 -73
- package/dist/bin/exe-search.js +989 -226
- package/dist/bin/exe-session-cleanup.js +4806 -1665
- package/dist/bin/exe-settings.js +20 -5
- package/dist/bin/exe-status.js +97 -2
- package/dist/bin/exe-team.js +97 -2
- package/dist/bin/git-sweep.js +899 -207
- package/dist/bin/graph-backfill.js +175 -72
- package/dist/bin/graph-export.js +175 -72
- package/dist/bin/install.js +38 -7
- package/dist/bin/list-providers.js +1 -0
- package/dist/bin/scan-tasks.js +904 -211
- package/dist/bin/setup.js +867 -268
- package/dist/bin/shard-migrate.js +175 -72
- package/dist/bin/update.js +1 -0
- package/dist/bin/wiki-sync.js +175 -72
- package/dist/gateway/index.js +548 -166
- package/dist/hooks/bug-report-worker.js +208 -23
- package/dist/hooks/commit-complete.js +897 -205
- package/dist/hooks/error-recall.js +988 -226
- package/dist/hooks/ingest-worker.js +1638 -1194
- package/dist/hooks/ingest.js +3 -0
- package/dist/hooks/instructions-loaded.js +707 -97
- package/dist/hooks/notification.js +699 -89
- package/dist/hooks/post-compact.js +714 -104
- package/dist/hooks/pre-compact.js +897 -205
- package/dist/hooks/pre-tool-use.js +742 -123
- package/dist/hooks/prompt-ingest-worker.js +242 -101
- package/dist/hooks/prompt-submit.js +995 -233
- package/dist/hooks/response-ingest-worker.js +242 -101
- package/dist/hooks/session-end.js +3941 -400
- package/dist/hooks/session-start.js +1001 -226
- package/dist/hooks/stop.js +725 -115
- package/dist/hooks/subagent-stop.js +714 -104
- package/dist/hooks/summary-worker.js +1964 -1330
- package/dist/index.js +1651 -1053
- package/dist/lib/cloud-sync.js +907 -86
- package/dist/lib/consolidation.js +2 -1
- package/dist/lib/database.js +642 -87
- package/dist/lib/db-daemon-client.js +503 -0
- package/dist/lib/device-registry.js +547 -7
- package/dist/lib/embedder.js +14 -28
- package/dist/lib/employee-templates.js +84 -74
- package/dist/lib/employees.js +9 -0
- package/dist/lib/exe-daemon-client.js +16 -29
- package/dist/lib/exe-daemon.js +1955 -922
- package/dist/lib/hybrid-search.js +988 -226
- package/dist/lib/identity.js +87 -67
- package/dist/lib/keychain.js +9 -1
- package/dist/lib/messaging.js +8 -1
- package/dist/lib/reminders.js +91 -74
- package/dist/lib/schedules.js +96 -2
- package/dist/lib/skill-learning.js +103 -85
- package/dist/lib/store.js +234 -73
- package/dist/lib/tasks.js +111 -22
- package/dist/lib/tmux-routing.js +120 -31
- package/dist/lib/token-spend.js +273 -0
- package/dist/lib/ws-client.js +11 -0
- package/dist/mcp/server.js +5222 -475
- package/dist/mcp/tools/complete-reminder.js +94 -77
- package/dist/mcp/tools/create-reminder.js +94 -77
- package/dist/mcp/tools/create-task.js +120 -22
- package/dist/mcp/tools/deactivate-behavior.js +95 -77
- package/dist/mcp/tools/list-reminders.js +94 -77
- package/dist/mcp/tools/list-tasks.js +31 -1
- package/dist/mcp/tools/send-message.js +8 -1
- package/dist/mcp/tools/update-task.js +39 -10
- package/dist/runtime/index.js +911 -219
- package/dist/tui/App.js +997 -295
- package/package.json +6 -1
package/dist/lib/tasks.js
CHANGED
|
@@ -266,15 +266,22 @@ function getClient() {
|
|
|
266
266
|
if (!_resilientClient) {
|
|
267
267
|
throw new Error("Database client not initialized. Call initDatabase() first.");
|
|
268
268
|
}
|
|
269
|
+
if (process.env.EXE_IS_DAEMON === "1") {
|
|
270
|
+
return _resilientClient;
|
|
271
|
+
}
|
|
272
|
+
if (_daemonClient && _daemonClient._isDaemonActive()) {
|
|
273
|
+
return _daemonClient;
|
|
274
|
+
}
|
|
269
275
|
return _resilientClient;
|
|
270
276
|
}
|
|
271
|
-
var _resilientClient;
|
|
277
|
+
var _resilientClient, _daemonClient;
|
|
272
278
|
var init_database = __esm({
|
|
273
279
|
"src/lib/database.ts"() {
|
|
274
280
|
"use strict";
|
|
275
281
|
init_db_retry();
|
|
276
282
|
init_employees();
|
|
277
283
|
_resilientClient = null;
|
|
284
|
+
_daemonClient = null;
|
|
278
285
|
}
|
|
279
286
|
});
|
|
280
287
|
|
|
@@ -1454,7 +1461,7 @@ function notifyParentExe(sessionKey) {
|
|
|
1454
1461
|
return true;
|
|
1455
1462
|
}
|
|
1456
1463
|
function ensureEmployee(employeeName, exeSession, projectDir, opts) {
|
|
1457
|
-
if (
|
|
1464
|
+
if (isCoordinatorName(employeeName)) {
|
|
1458
1465
|
return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
|
|
1459
1466
|
}
|
|
1460
1467
|
try {
|
|
@@ -1787,6 +1794,7 @@ var init_task_scope = __esm({
|
|
|
1787
1794
|
// src/lib/tasks-crud.ts
|
|
1788
1795
|
import crypto3 from "crypto";
|
|
1789
1796
|
import path9 from "path";
|
|
1797
|
+
import os7 from "os";
|
|
1790
1798
|
import { execSync as execSync5 } from "child_process";
|
|
1791
1799
|
import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
|
|
1792
1800
|
import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
|
|
@@ -1830,6 +1838,35 @@ function extractParentFromContext(contextBody) {
|
|
|
1830
1838
|
function slugify(title) {
|
|
1831
1839
|
return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
1832
1840
|
}
|
|
1841
|
+
function buildKeywordIndex() {
|
|
1842
|
+
const idx = /* @__PURE__ */ new Map();
|
|
1843
|
+
for (const [role, keywords] of Object.entries(LANE_KEYWORDS)) {
|
|
1844
|
+
for (const kw of keywords) {
|
|
1845
|
+
const existing = idx.get(kw) ?? [];
|
|
1846
|
+
existing.push(role);
|
|
1847
|
+
idx.set(kw, existing);
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
return idx;
|
|
1851
|
+
}
|
|
1852
|
+
function checkLaneAffinity(title, context, assigneeName) {
|
|
1853
|
+
const employees = loadEmployeesSync();
|
|
1854
|
+
const employee = employees.find((e) => e.name === assigneeName);
|
|
1855
|
+
if (!employee) return void 0;
|
|
1856
|
+
const assigneeRole = employee.role;
|
|
1857
|
+
const text = `${title} ${context}`.toLowerCase();
|
|
1858
|
+
const matchedRoles = /* @__PURE__ */ new Set();
|
|
1859
|
+
for (const [keyword, roles] of KEYWORD_INDEX) {
|
|
1860
|
+
if (text.includes(keyword)) {
|
|
1861
|
+
for (const role of roles) matchedRoles.add(role);
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
if (matchedRoles.size === 0) return void 0;
|
|
1865
|
+
if (matchedRoles.has(assigneeRole)) return void 0;
|
|
1866
|
+
if (assigneeRole === "COO") return void 0;
|
|
1867
|
+
const expectedRoles = Array.from(matchedRoles).join(" or ");
|
|
1868
|
+
return `\u26A0\uFE0F Lane mismatch: task content suggests ${expectedRoles}, but assigned to ${assigneeName} (${assigneeRole}).`;
|
|
1869
|
+
}
|
|
1833
1870
|
async function resolveTask(client, identifier, scopeSession) {
|
|
1834
1871
|
const scope = sessionScopeFilter(scopeSession);
|
|
1835
1872
|
let result = await client.execute({
|
|
@@ -1879,7 +1916,14 @@ async function createTaskCore(input) {
|
|
|
1879
1916
|
const id = crypto3.randomUUID();
|
|
1880
1917
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1881
1918
|
const slug = slugify(input.title);
|
|
1882
|
-
|
|
1919
|
+
let earlySessionScope = null;
|
|
1920
|
+
try {
|
|
1921
|
+
const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
|
|
1922
|
+
earlySessionScope = resolveExeSession2();
|
|
1923
|
+
} catch {
|
|
1924
|
+
}
|
|
1925
|
+
const scope = earlySessionScope ?? "default";
|
|
1926
|
+
const taskFile = input.taskFile ?? `tasks/${scope}/${input.assignedTo}/${slug}.md`;
|
|
1883
1927
|
let blockedById = null;
|
|
1884
1928
|
const initialStatus = input.blockedBy ? "blocked" : "open";
|
|
1885
1929
|
if (input.blockedBy) {
|
|
@@ -1919,6 +1963,13 @@ async function createTaskCore(input) {
|
|
|
1919
1963
|
if (dupCheck.rows.length > 0) {
|
|
1920
1964
|
warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
|
|
1921
1965
|
}
|
|
1966
|
+
if (!process.env.DISABLE_LANE_AFFINITY) {
|
|
1967
|
+
const laneWarning = checkLaneAffinity(input.title, input.context, input.assignedTo);
|
|
1968
|
+
if (laneWarning) {
|
|
1969
|
+
warning = warning ? `${warning}
|
|
1970
|
+
${laneWarning}` : laneWarning;
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1922
1973
|
if (input.baseDir) {
|
|
1923
1974
|
try {
|
|
1924
1975
|
await mkdir3(path9.join(input.baseDir, "exe", "output"), { recursive: true });
|
|
@@ -1929,12 +1980,7 @@ async function createTaskCore(input) {
|
|
|
1929
1980
|
}
|
|
1930
1981
|
}
|
|
1931
1982
|
const complexity = input.complexity ?? "standard";
|
|
1932
|
-
|
|
1933
|
-
try {
|
|
1934
|
-
const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
|
|
1935
|
-
sessionScope = resolveExeSession2();
|
|
1936
|
-
} catch {
|
|
1937
|
-
}
|
|
1983
|
+
const sessionScope = earlySessionScope;
|
|
1938
1984
|
await client.execute({
|
|
1939
1985
|
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)
|
|
1940
1986
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
@@ -1961,6 +2007,39 @@ async function createTaskCore(input) {
|
|
|
1961
2007
|
now
|
|
1962
2008
|
]
|
|
1963
2009
|
});
|
|
2010
|
+
if (input.baseDir) {
|
|
2011
|
+
try {
|
|
2012
|
+
const EXE_OS_DIR = path9.join(os7.homedir(), ".exe-os");
|
|
2013
|
+
const mdPath = path9.join(EXE_OS_DIR, taskFile);
|
|
2014
|
+
const mdDir = path9.dirname(mdPath);
|
|
2015
|
+
if (!existsSync9(mdDir)) await mkdir3(mdDir, { recursive: true });
|
|
2016
|
+
const reviewer = input.reviewer ?? input.assignedBy;
|
|
2017
|
+
const mdContent = `# ${input.title}
|
|
2018
|
+
|
|
2019
|
+
**ID:** ${id}
|
|
2020
|
+
**Status:** ${initialStatus}
|
|
2021
|
+
**Priority:** ${input.priority}
|
|
2022
|
+
**Assigned by:** ${input.assignedBy}
|
|
2023
|
+
**Assigned to:** ${input.assignedTo}
|
|
2024
|
+
**Project:** ${input.projectName}
|
|
2025
|
+
**Created:** ${now.split("T")[0]}${parentTaskId ? `
|
|
2026
|
+
**Parent task:** ${parentTaskId}` : ""}
|
|
2027
|
+
**Reviewer:** ${reviewer}
|
|
2028
|
+
|
|
2029
|
+
## Context
|
|
2030
|
+
|
|
2031
|
+
${input.context}
|
|
2032
|
+
|
|
2033
|
+
## MANDATORY: When done
|
|
2034
|
+
|
|
2035
|
+
You MUST call update_task with status "done" and a result summary when finished.
|
|
2036
|
+
If you skip this, your reviewer will not know you're done and your work won't be reviewed.
|
|
2037
|
+
Do NOT let a failed commit or any error prevent you from calling update_task(done).
|
|
2038
|
+
`;
|
|
2039
|
+
await writeFile3(mdPath, mdContent, "utf-8");
|
|
2040
|
+
} catch {
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
1964
2043
|
return {
|
|
1965
2044
|
id,
|
|
1966
2045
|
title: input.title,
|
|
@@ -2153,7 +2232,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
|
|
|
2153
2232
|
return { row, taskFile, now, taskId };
|
|
2154
2233
|
}
|
|
2155
2234
|
}
|
|
2156
|
-
if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || input.callerAgentId
|
|
2235
|
+
if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || isCoordinatorName(input.callerAgentId))) {
|
|
2157
2236
|
process.stderr.write(
|
|
2158
2237
|
`[tasks] Assigner override: ${input.callerAgentId} reclaiming ${taskId}
|
|
2159
2238
|
`
|
|
@@ -2265,12 +2344,22 @@ async function ensureGitignoreExe(baseDir) {
|
|
|
2265
2344
|
} catch {
|
|
2266
2345
|
}
|
|
2267
2346
|
}
|
|
2268
|
-
var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
|
|
2347
|
+
var LANE_KEYWORDS, KEYWORD_INDEX, DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
|
|
2269
2348
|
var init_tasks_crud = __esm({
|
|
2270
2349
|
"src/lib/tasks-crud.ts"() {
|
|
2271
2350
|
"use strict";
|
|
2272
2351
|
init_database();
|
|
2273
2352
|
init_task_scope();
|
|
2353
|
+
init_employees();
|
|
2354
|
+
LANE_KEYWORDS = {
|
|
2355
|
+
CMO: ["sales", "script", "pitch", "offer", "copy", "objection", "brand", "content", "seo", "marketing", "newsletter", "carousel", "social", "campaign"],
|
|
2356
|
+
CTO: ["spec", "architecture", "migration", "schema", "database", "design doc", "adr", "security audit", "tech stack"],
|
|
2357
|
+
"Principal Engineer": ["implement", "build", "fix", "commit", "refactor", "bug", "feature", "wire", "integration"],
|
|
2358
|
+
"Staff Code Reviewer": ["critique", "verdict", "review", "audit", "code quality"],
|
|
2359
|
+
"Content Production Specialist": ["render", "video", "image", "b-roll", "remotion", "animation", "thumbnail"],
|
|
2360
|
+
"AI Product Lead": ["competitive", "analysis", "benchmark", "compare", "scout", "evaluate", "poc"]
|
|
2361
|
+
};
|
|
2362
|
+
KEYWORD_INDEX = buildKeywordIndex();
|
|
2274
2363
|
DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
|
|
2275
2364
|
TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
|
|
2276
2365
|
}
|
|
@@ -2300,7 +2389,7 @@ async function countNewPendingReviewsSince(sinceIso, sessionScope) {
|
|
|
2300
2389
|
const result2 = await client.execute({
|
|
2301
2390
|
sql: `SELECT COUNT(*) as cnt FROM tasks
|
|
2302
2391
|
WHERE status = 'needs_review' AND updated_at > ?
|
|
2303
|
-
AND
|
|
2392
|
+
AND session_scope = ?`,
|
|
2304
2393
|
args: [sinceIso, sessionScope]
|
|
2305
2394
|
});
|
|
2306
2395
|
return Number(result2.rows[0]?.cnt) || 0;
|
|
@@ -2318,7 +2407,7 @@ async function listPendingReviews(limit, sessionScope) {
|
|
|
2318
2407
|
const result2 = await client.execute({
|
|
2319
2408
|
sql: `SELECT title, assigned_to, project_name FROM tasks
|
|
2320
2409
|
WHERE status = 'needs_review'
|
|
2321
|
-
AND
|
|
2410
|
+
AND session_scope = ?
|
|
2322
2411
|
ORDER BY priority ASC, created_at DESC LIMIT ?`,
|
|
2323
2412
|
args: [sessionScope, limit]
|
|
2324
2413
|
});
|
|
@@ -2439,14 +2528,14 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
|
|
|
2439
2528
|
if (parts.length >= 3 && parts[0] === "review") {
|
|
2440
2529
|
const agent = parts[1];
|
|
2441
2530
|
const slug = parts.slice(2).join("-");
|
|
2442
|
-
const
|
|
2531
|
+
const legacyTaskFile = `exe/${agent}/${slug}.md`;
|
|
2443
2532
|
const result = await client.execute({
|
|
2444
|
-
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
|
|
2445
|
-
args: [now,
|
|
2533
|
+
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
|
|
2534
|
+
args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
|
|
2446
2535
|
});
|
|
2447
2536
|
if (result.rowsAffected > 0) {
|
|
2448
2537
|
process.stderr.write(
|
|
2449
|
-
`[review-cleanup] Cascaded original task to done
|
|
2538
|
+
`[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
|
|
2450
2539
|
`
|
|
2451
2540
|
);
|
|
2452
2541
|
}
|
|
@@ -2628,7 +2717,7 @@ function findSessionForProject(projectName) {
|
|
|
2628
2717
|
const sessions = listSessions();
|
|
2629
2718
|
for (const s of sessions) {
|
|
2630
2719
|
const proj = s.projectDir.split("/").filter(Boolean).pop();
|
|
2631
|
-
if (proj === projectName &&
|
|
2720
|
+
if (proj === projectName && isCoordinatorName(s.agentId)) return s;
|
|
2632
2721
|
}
|
|
2633
2722
|
return null;
|
|
2634
2723
|
}
|
|
@@ -2674,7 +2763,7 @@ var init_session_scope = __esm({
|
|
|
2674
2763
|
|
|
2675
2764
|
// src/lib/tasks-notify.ts
|
|
2676
2765
|
async function dispatchTaskToEmployee(input) {
|
|
2677
|
-
if (
|
|
2766
|
+
if (isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
|
|
2678
2767
|
let crossProject = false;
|
|
2679
2768
|
if (input.projectName) {
|
|
2680
2769
|
try {
|
|
@@ -3153,7 +3242,7 @@ async function updateTask(input) {
|
|
|
3153
3242
|
}
|
|
3154
3243
|
const isTerminal = input.status === "done" || input.status === "needs_review";
|
|
3155
3244
|
if (isTerminal) {
|
|
3156
|
-
const isCoordinator =
|
|
3245
|
+
const isCoordinator = isCoordinatorName(String(row.assigned_to));
|
|
3157
3246
|
if (!isCoordinator) {
|
|
3158
3247
|
notifyTaskDone();
|
|
3159
3248
|
}
|
|
@@ -3178,7 +3267,7 @@ async function updateTask(input) {
|
|
|
3178
3267
|
}
|
|
3179
3268
|
}
|
|
3180
3269
|
}
|
|
3181
|
-
if (input.status === "done" &&
|
|
3270
|
+
if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
|
|
3182
3271
|
Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
|
|
3183
3272
|
({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
|
|
3184
3273
|
taskId,
|
|
@@ -3194,7 +3283,7 @@ async function updateTask(input) {
|
|
|
3194
3283
|
});
|
|
3195
3284
|
}
|
|
3196
3285
|
let nextTask;
|
|
3197
|
-
if (isTerminal &&
|
|
3286
|
+
if (isTerminal && !isCoordinatorName(String(row.assigned_to))) {
|
|
3198
3287
|
try {
|
|
3199
3288
|
nextTask = await findNextTask(String(row.assigned_to));
|
|
3200
3289
|
} catch {
|
package/dist/lib/tmux-routing.js
CHANGED
|
@@ -564,15 +564,22 @@ function getClient() {
|
|
|
564
564
|
if (!_resilientClient) {
|
|
565
565
|
throw new Error("Database client not initialized. Call initDatabase() first.");
|
|
566
566
|
}
|
|
567
|
+
if (process.env.EXE_IS_DAEMON === "1") {
|
|
568
|
+
return _resilientClient;
|
|
569
|
+
}
|
|
570
|
+
if (_daemonClient && _daemonClient._isDaemonActive()) {
|
|
571
|
+
return _daemonClient;
|
|
572
|
+
}
|
|
567
573
|
return _resilientClient;
|
|
568
574
|
}
|
|
569
|
-
var _resilientClient;
|
|
575
|
+
var _resilientClient, _daemonClient;
|
|
570
576
|
var init_database = __esm({
|
|
571
577
|
"src/lib/database.ts"() {
|
|
572
578
|
"use strict";
|
|
573
579
|
init_db_retry();
|
|
574
580
|
init_employees();
|
|
575
581
|
_resilientClient = null;
|
|
582
|
+
_daemonClient = null;
|
|
576
583
|
}
|
|
577
584
|
});
|
|
578
585
|
|
|
@@ -845,6 +852,7 @@ var init_state_bus = __esm({
|
|
|
845
852
|
// src/lib/tasks-crud.ts
|
|
846
853
|
import crypto3 from "crypto";
|
|
847
854
|
import path8 from "path";
|
|
855
|
+
import os6 from "os";
|
|
848
856
|
import { execSync as execSync4 } from "child_process";
|
|
849
857
|
import { mkdir as mkdir3, writeFile as writeFile3, appendFile } from "fs/promises";
|
|
850
858
|
import { existsSync as existsSync8, readFileSync as readFileSync8 } from "fs";
|
|
@@ -888,6 +896,35 @@ function extractParentFromContext(contextBody) {
|
|
|
888
896
|
function slugify(title) {
|
|
889
897
|
return title.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
890
898
|
}
|
|
899
|
+
function buildKeywordIndex() {
|
|
900
|
+
const idx = /* @__PURE__ */ new Map();
|
|
901
|
+
for (const [role, keywords] of Object.entries(LANE_KEYWORDS)) {
|
|
902
|
+
for (const kw of keywords) {
|
|
903
|
+
const existing = idx.get(kw) ?? [];
|
|
904
|
+
existing.push(role);
|
|
905
|
+
idx.set(kw, existing);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
return idx;
|
|
909
|
+
}
|
|
910
|
+
function checkLaneAffinity(title, context, assigneeName) {
|
|
911
|
+
const employees = loadEmployeesSync();
|
|
912
|
+
const employee = employees.find((e) => e.name === assigneeName);
|
|
913
|
+
if (!employee) return void 0;
|
|
914
|
+
const assigneeRole = employee.role;
|
|
915
|
+
const text = `${title} ${context}`.toLowerCase();
|
|
916
|
+
const matchedRoles = /* @__PURE__ */ new Set();
|
|
917
|
+
for (const [keyword, roles] of KEYWORD_INDEX) {
|
|
918
|
+
if (text.includes(keyword)) {
|
|
919
|
+
for (const role of roles) matchedRoles.add(role);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
if (matchedRoles.size === 0) return void 0;
|
|
923
|
+
if (matchedRoles.has(assigneeRole)) return void 0;
|
|
924
|
+
if (assigneeRole === "COO") return void 0;
|
|
925
|
+
const expectedRoles = Array.from(matchedRoles).join(" or ");
|
|
926
|
+
return `\u26A0\uFE0F Lane mismatch: task content suggests ${expectedRoles}, but assigned to ${assigneeName} (${assigneeRole}).`;
|
|
927
|
+
}
|
|
891
928
|
async function resolveTask(client, identifier, scopeSession) {
|
|
892
929
|
const scope = sessionScopeFilter(scopeSession);
|
|
893
930
|
let result = await client.execute({
|
|
@@ -937,7 +974,14 @@ async function createTaskCore(input) {
|
|
|
937
974
|
const id = crypto3.randomUUID();
|
|
938
975
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
939
976
|
const slug = slugify(input.title);
|
|
940
|
-
|
|
977
|
+
let earlySessionScope = null;
|
|
978
|
+
try {
|
|
979
|
+
const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
|
|
980
|
+
earlySessionScope = resolveExeSession2();
|
|
981
|
+
} catch {
|
|
982
|
+
}
|
|
983
|
+
const scope = earlySessionScope ?? "default";
|
|
984
|
+
const taskFile = input.taskFile ?? `tasks/${scope}/${input.assignedTo}/${slug}.md`;
|
|
941
985
|
let blockedById = null;
|
|
942
986
|
const initialStatus = input.blockedBy ? "blocked" : "open";
|
|
943
987
|
if (input.blockedBy) {
|
|
@@ -977,6 +1021,13 @@ async function createTaskCore(input) {
|
|
|
977
1021
|
if (dupCheck.rows.length > 0) {
|
|
978
1022
|
warning = `similar active task already exists (${String(dupCheck.rows[0].id)}). Created new task anyway.`;
|
|
979
1023
|
}
|
|
1024
|
+
if (!process.env.DISABLE_LANE_AFFINITY) {
|
|
1025
|
+
const laneWarning = checkLaneAffinity(input.title, input.context, input.assignedTo);
|
|
1026
|
+
if (laneWarning) {
|
|
1027
|
+
warning = warning ? `${warning}
|
|
1028
|
+
${laneWarning}` : laneWarning;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
980
1031
|
if (input.baseDir) {
|
|
981
1032
|
try {
|
|
982
1033
|
await mkdir3(path8.join(input.baseDir, "exe", "output"), { recursive: true });
|
|
@@ -987,12 +1038,7 @@ async function createTaskCore(input) {
|
|
|
987
1038
|
}
|
|
988
1039
|
}
|
|
989
1040
|
const complexity = input.complexity ?? "standard";
|
|
990
|
-
|
|
991
|
-
try {
|
|
992
|
-
const { resolveExeSession: resolveExeSession2 } = await Promise.resolve().then(() => (init_tmux_routing(), tmux_routing_exports));
|
|
993
|
-
sessionScope = resolveExeSession2();
|
|
994
|
-
} catch {
|
|
995
|
-
}
|
|
1041
|
+
const sessionScope = earlySessionScope;
|
|
996
1042
|
await client.execute({
|
|
997
1043
|
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)
|
|
998
1044
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
@@ -1019,6 +1065,39 @@ async function createTaskCore(input) {
|
|
|
1019
1065
|
now
|
|
1020
1066
|
]
|
|
1021
1067
|
});
|
|
1068
|
+
if (input.baseDir) {
|
|
1069
|
+
try {
|
|
1070
|
+
const EXE_OS_DIR = path8.join(os6.homedir(), ".exe-os");
|
|
1071
|
+
const mdPath = path8.join(EXE_OS_DIR, taskFile);
|
|
1072
|
+
const mdDir = path8.dirname(mdPath);
|
|
1073
|
+
if (!existsSync8(mdDir)) await mkdir3(mdDir, { recursive: true });
|
|
1074
|
+
const reviewer = input.reviewer ?? input.assignedBy;
|
|
1075
|
+
const mdContent = `# ${input.title}
|
|
1076
|
+
|
|
1077
|
+
**ID:** ${id}
|
|
1078
|
+
**Status:** ${initialStatus}
|
|
1079
|
+
**Priority:** ${input.priority}
|
|
1080
|
+
**Assigned by:** ${input.assignedBy}
|
|
1081
|
+
**Assigned to:** ${input.assignedTo}
|
|
1082
|
+
**Project:** ${input.projectName}
|
|
1083
|
+
**Created:** ${now.split("T")[0]}${parentTaskId ? `
|
|
1084
|
+
**Parent task:** ${parentTaskId}` : ""}
|
|
1085
|
+
**Reviewer:** ${reviewer}
|
|
1086
|
+
|
|
1087
|
+
## Context
|
|
1088
|
+
|
|
1089
|
+
${input.context}
|
|
1090
|
+
|
|
1091
|
+
## MANDATORY: When done
|
|
1092
|
+
|
|
1093
|
+
You MUST call update_task with status "done" and a result summary when finished.
|
|
1094
|
+
If you skip this, your reviewer will not know you're done and your work won't be reviewed.
|
|
1095
|
+
Do NOT let a failed commit or any error prevent you from calling update_task(done).
|
|
1096
|
+
`;
|
|
1097
|
+
await writeFile3(mdPath, mdContent, "utf-8");
|
|
1098
|
+
} catch {
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1022
1101
|
return {
|
|
1023
1102
|
id,
|
|
1024
1103
|
title: input.title,
|
|
@@ -1211,7 +1290,7 @@ ${input.result}` : `\u26A0\uFE0F ${warning}`;
|
|
|
1211
1290
|
return { row, taskFile, now, taskId };
|
|
1212
1291
|
}
|
|
1213
1292
|
}
|
|
1214
|
-
if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || input.callerAgentId
|
|
1293
|
+
if (curStatus === "in_progress" && input.callerAgentId && (input.callerAgentId === assignedBy || isCoordinatorName(input.callerAgentId))) {
|
|
1215
1294
|
process.stderr.write(
|
|
1216
1295
|
`[tasks] Assigner override: ${input.callerAgentId} reclaiming ${taskId}
|
|
1217
1296
|
`
|
|
@@ -1323,12 +1402,22 @@ async function ensureGitignoreExe(baseDir) {
|
|
|
1323
1402
|
} catch {
|
|
1324
1403
|
}
|
|
1325
1404
|
}
|
|
1326
|
-
var DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
|
|
1405
|
+
var LANE_KEYWORDS, KEYWORD_INDEX, DELEGATION_KEYWORDS, TASK_ALREADY_CLAIMED_PREFIX;
|
|
1327
1406
|
var init_tasks_crud = __esm({
|
|
1328
1407
|
"src/lib/tasks-crud.ts"() {
|
|
1329
1408
|
"use strict";
|
|
1330
1409
|
init_database();
|
|
1331
1410
|
init_task_scope();
|
|
1411
|
+
init_employees();
|
|
1412
|
+
LANE_KEYWORDS = {
|
|
1413
|
+
CMO: ["sales", "script", "pitch", "offer", "copy", "objection", "brand", "content", "seo", "marketing", "newsletter", "carousel", "social", "campaign"],
|
|
1414
|
+
CTO: ["spec", "architecture", "migration", "schema", "database", "design doc", "adr", "security audit", "tech stack"],
|
|
1415
|
+
"Principal Engineer": ["implement", "build", "fix", "commit", "refactor", "bug", "feature", "wire", "integration"],
|
|
1416
|
+
"Staff Code Reviewer": ["critique", "verdict", "review", "audit", "code quality"],
|
|
1417
|
+
"Content Production Specialist": ["render", "video", "image", "b-roll", "remotion", "animation", "thumbnail"],
|
|
1418
|
+
"AI Product Lead": ["competitive", "analysis", "benchmark", "compare", "scout", "evaluate", "poc"]
|
|
1419
|
+
};
|
|
1420
|
+
KEYWORD_INDEX = buildKeywordIndex();
|
|
1332
1421
|
DELEGATION_KEYWORDS = /parallel|delegate|wave|worktree|multi-instance/i;
|
|
1333
1422
|
TASK_ALREADY_CLAIMED_PREFIX = "TASK_ALREADY_CLAIMED";
|
|
1334
1423
|
}
|
|
@@ -1358,7 +1447,7 @@ async function countNewPendingReviewsSince(sinceIso, sessionScope) {
|
|
|
1358
1447
|
const result2 = await client.execute({
|
|
1359
1448
|
sql: `SELECT COUNT(*) as cnt FROM tasks
|
|
1360
1449
|
WHERE status = 'needs_review' AND updated_at > ?
|
|
1361
|
-
AND
|
|
1450
|
+
AND session_scope = ?`,
|
|
1362
1451
|
args: [sinceIso, sessionScope]
|
|
1363
1452
|
});
|
|
1364
1453
|
return Number(result2.rows[0]?.cnt) || 0;
|
|
@@ -1376,7 +1465,7 @@ async function listPendingReviews(limit, sessionScope) {
|
|
|
1376
1465
|
const result2 = await client.execute({
|
|
1377
1466
|
sql: `SELECT title, assigned_to, project_name FROM tasks
|
|
1378
1467
|
WHERE status = 'needs_review'
|
|
1379
|
-
AND
|
|
1468
|
+
AND session_scope = ?
|
|
1380
1469
|
ORDER BY priority ASC, created_at DESC LIMIT ?`,
|
|
1381
1470
|
args: [sessionScope, limit]
|
|
1382
1471
|
});
|
|
@@ -1497,14 +1586,14 @@ async function cleanupReviewFile(row, taskFile, _baseDir) {
|
|
|
1497
1586
|
if (parts.length >= 3 && parts[0] === "review") {
|
|
1498
1587
|
const agent = parts[1];
|
|
1499
1588
|
const slug = parts.slice(2).join("-");
|
|
1500
|
-
const
|
|
1589
|
+
const legacyTaskFile = `exe/${agent}/${slug}.md`;
|
|
1501
1590
|
const result = await client.execute({
|
|
1502
|
-
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE task_file = ? AND status = 'needs_review'",
|
|
1503
|
-
args: [now,
|
|
1591
|
+
sql: "UPDATE tasks SET status = 'done', updated_at = ? WHERE (task_file = ? OR task_file LIKE ?) AND status = 'needs_review'",
|
|
1592
|
+
args: [now, legacyTaskFile, `tasks/%/${agent}/${slug}.md`]
|
|
1504
1593
|
});
|
|
1505
1594
|
if (result.rowsAffected > 0) {
|
|
1506
1595
|
process.stderr.write(
|
|
1507
|
-
`[review-cleanup] Cascaded original task to done
|
|
1596
|
+
`[review-cleanup] Cascaded original task to done: ${agent}/${slug}.md
|
|
1508
1597
|
`
|
|
1509
1598
|
);
|
|
1510
1599
|
}
|
|
@@ -1686,7 +1775,7 @@ function findSessionForProject(projectName) {
|
|
|
1686
1775
|
const sessions = listSessions();
|
|
1687
1776
|
for (const s of sessions) {
|
|
1688
1777
|
const proj = s.projectDir.split("/").filter(Boolean).pop();
|
|
1689
|
-
if (proj === projectName &&
|
|
1778
|
+
if (proj === projectName && isCoordinatorName(s.agentId)) return s;
|
|
1690
1779
|
}
|
|
1691
1780
|
return null;
|
|
1692
1781
|
}
|
|
@@ -1732,7 +1821,7 @@ var init_session_scope = __esm({
|
|
|
1732
1821
|
|
|
1733
1822
|
// src/lib/tasks-notify.ts
|
|
1734
1823
|
async function dispatchTaskToEmployee(input) {
|
|
1735
|
-
if (
|
|
1824
|
+
if (isCoordinatorName(input.assignedTo)) return { dispatched: "skipped" };
|
|
1736
1825
|
let crossProject = false;
|
|
1737
1826
|
if (input.projectName) {
|
|
1738
1827
|
try {
|
|
@@ -2211,7 +2300,7 @@ async function updateTask(input) {
|
|
|
2211
2300
|
}
|
|
2212
2301
|
const isTerminal = input.status === "done" || input.status === "needs_review";
|
|
2213
2302
|
if (isTerminal) {
|
|
2214
|
-
const isCoordinator =
|
|
2303
|
+
const isCoordinator = isCoordinatorName(String(row.assigned_to));
|
|
2215
2304
|
if (!isCoordinator) {
|
|
2216
2305
|
notifyTaskDone();
|
|
2217
2306
|
}
|
|
@@ -2236,7 +2325,7 @@ async function updateTask(input) {
|
|
|
2236
2325
|
}
|
|
2237
2326
|
}
|
|
2238
2327
|
}
|
|
2239
|
-
if (input.status === "done" &&
|
|
2328
|
+
if (input.status === "done" && !isCoordinatorName(String(row.assigned_to)) && !process.env.VITEST) {
|
|
2240
2329
|
Promise.resolve().then(() => (init_skill_learning(), skill_learning_exports)).then(
|
|
2241
2330
|
({ captureAndLearn: captureAndLearn2 }) => captureAndLearn2({
|
|
2242
2331
|
taskId,
|
|
@@ -2252,7 +2341,7 @@ async function updateTask(input) {
|
|
|
2252
2341
|
});
|
|
2253
2342
|
}
|
|
2254
2343
|
let nextTask;
|
|
2255
|
-
if (isTerminal &&
|
|
2344
|
+
if (isTerminal && !isCoordinatorName(String(row.assigned_to))) {
|
|
2256
2345
|
try {
|
|
2257
2346
|
nextTask = await findNextTask(String(row.assigned_to));
|
|
2258
2347
|
} catch {
|
|
@@ -2620,7 +2709,7 @@ __export(tmux_routing_exports, {
|
|
|
2620
2709
|
import { execFileSync as execFileSync2, execSync as execSync6 } from "child_process";
|
|
2621
2710
|
import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, mkdirSync as mkdirSync5, existsSync as existsSync10, appendFileSync } from "fs";
|
|
2622
2711
|
import path13 from "path";
|
|
2623
|
-
import
|
|
2712
|
+
import os7 from "os";
|
|
2624
2713
|
import { fileURLToPath } from "url";
|
|
2625
2714
|
import { unlinkSync as unlinkSync5 } from "fs";
|
|
2626
2715
|
function spawnLockPath(sessionName) {
|
|
@@ -2944,7 +3033,7 @@ function notifyParentExe(sessionKey) {
|
|
|
2944
3033
|
return true;
|
|
2945
3034
|
}
|
|
2946
3035
|
function ensureEmployee(employeeName, exeSession, projectDir, opts) {
|
|
2947
|
-
if (
|
|
3036
|
+
if (isCoordinatorName(employeeName)) {
|
|
2948
3037
|
return { status: "failed", sessionName: "", error: "The COO is not a dispatchable employee" };
|
|
2949
3038
|
}
|
|
2950
3039
|
try {
|
|
@@ -3016,7 +3105,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
3016
3105
|
const transport = getTransport();
|
|
3017
3106
|
const sessionName = employeeSessionName(employeeName, exeSession, opts?.instance);
|
|
3018
3107
|
const instanceLabel = opts?.instance != null && opts.instance > 0 ? `${employeeName}${opts.instance}` : employeeName;
|
|
3019
|
-
const logDir = path13.join(
|
|
3108
|
+
const logDir = path13.join(os7.homedir(), ".exe-os", "session-logs");
|
|
3020
3109
|
const logFile = path13.join(logDir, `${instanceLabel}-${Date.now()}.log`);
|
|
3021
3110
|
if (!existsSync10(logDir)) {
|
|
3022
3111
|
mkdirSync5(logDir, { recursive: true });
|
|
@@ -3032,7 +3121,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
3032
3121
|
} catch {
|
|
3033
3122
|
}
|
|
3034
3123
|
try {
|
|
3035
|
-
const claudeJsonPath = path13.join(
|
|
3124
|
+
const claudeJsonPath = path13.join(os7.homedir(), ".claude.json");
|
|
3036
3125
|
let claudeJson = {};
|
|
3037
3126
|
try {
|
|
3038
3127
|
claudeJson = JSON.parse(readFileSync9(claudeJsonPath, "utf8"));
|
|
@@ -3047,7 +3136,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
3047
3136
|
} catch {
|
|
3048
3137
|
}
|
|
3049
3138
|
try {
|
|
3050
|
-
const settingsDir = path13.join(
|
|
3139
|
+
const settingsDir = path13.join(os7.homedir(), ".claude", "projects");
|
|
3051
3140
|
const normalizedKey = (opts?.cwd ?? projectDir).replace(/\//g, "-").replace(/^-/, "");
|
|
3052
3141
|
const projSettingsDir = path13.join(settingsDir, normalizedKey);
|
|
3053
3142
|
const settingsPath = path13.join(projSettingsDir, "settings.json");
|
|
@@ -3095,7 +3184,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
3095
3184
|
let legacyFallbackWarned = false;
|
|
3096
3185
|
if (!useExeAgent && !useBinSymlink) {
|
|
3097
3186
|
const identityPath = path13.join(
|
|
3098
|
-
|
|
3187
|
+
os7.homedir(),
|
|
3099
3188
|
".exe-os",
|
|
3100
3189
|
"identity",
|
|
3101
3190
|
`${employeeName}.md`
|
|
@@ -3125,7 +3214,7 @@ function spawnEmployee(employeeName, exeSession, projectDir, opts) {
|
|
|
3125
3214
|
}
|
|
3126
3215
|
let sessionContextFlag = "";
|
|
3127
3216
|
try {
|
|
3128
|
-
const ctxDir = path13.join(
|
|
3217
|
+
const ctxDir = path13.join(os7.homedir(), ".exe-os", "session-cache");
|
|
3129
3218
|
mkdirSync5(ctxDir, { recursive: true });
|
|
3130
3219
|
const ctxFile = path13.join(ctxDir, `session-context-${sessionName}.md`);
|
|
3131
3220
|
const ctxContent = [
|
|
@@ -3236,13 +3325,13 @@ var init_tmux_routing = __esm({
|
|
|
3236
3325
|
init_intercom_queue();
|
|
3237
3326
|
init_plan_limits();
|
|
3238
3327
|
init_employees();
|
|
3239
|
-
SPAWN_LOCK_DIR = path13.join(
|
|
3240
|
-
SESSION_CACHE = path13.join(
|
|
3328
|
+
SPAWN_LOCK_DIR = path13.join(os7.homedir(), ".exe-os", "spawn-locks");
|
|
3329
|
+
SESSION_CACHE = path13.join(os7.homedir(), ".exe-os", "session-cache");
|
|
3241
3330
|
BEHAVIORS_EXPORT_TIMEOUT_MS = 1e4;
|
|
3242
3331
|
VALID_SESSION_NAME = /^[a-z]+\d*-[a-zA-Z0-9_]+$/;
|
|
3243
3332
|
VERIFY_PANE_LINES = 200;
|
|
3244
3333
|
INTERCOM_DEBOUNCE_MS = 3e4;
|
|
3245
|
-
INTERCOM_LOG2 = path13.join(
|
|
3334
|
+
INTERCOM_LOG2 = path13.join(os7.homedir(), ".exe-os", "intercom.log");
|
|
3246
3335
|
DEBOUNCE_FILE = path13.join(SESSION_CACHE, "intercom-debounce.json");
|
|
3247
3336
|
DEBOUNCE_CLEANUP_AGE_MS = 5 * 60 * 1e3;
|
|
3248
3337
|
BUSY_PATTERN = /[✻✽✶✳·].*…|Running…/;
|