@hasna/todos 0.10.22 → 0.11.2

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/mcp/index.js CHANGED
@@ -392,6 +392,15 @@ function ensureSchema(db) {
392
392
  ensureColumn("tasks", "assigned_by", "TEXT");
393
393
  ensureColumn("tasks", "assigned_from_project", "TEXT");
394
394
  ensureColumn("tasks", "started_at", "TEXT");
395
+ ensureColumn("tasks", "task_type", "TEXT");
396
+ ensureColumn("tasks", "cost_tokens", "INTEGER DEFAULT 0");
397
+ ensureColumn("tasks", "cost_usd", "REAL DEFAULT 0");
398
+ ensureColumn("tasks", "delegated_from", "TEXT");
399
+ ensureColumn("tasks", "delegation_depth", "INTEGER DEFAULT 0");
400
+ ensureColumn("tasks", "retry_count", "INTEGER DEFAULT 0");
401
+ ensureColumn("tasks", "max_retries", "INTEGER DEFAULT 3");
402
+ ensureColumn("tasks", "retry_after", "TEXT");
403
+ ensureColumn("tasks", "sla_minutes", "INTEGER");
395
404
  ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
396
405
  ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
397
406
  ensureColumn("agents", "reports_to", "TEXT");
@@ -1005,6 +1014,58 @@ var init_database = __esm(() => {
1005
1014
  `
1006
1015
  ALTER TABLE tasks ADD COLUMN task_type TEXT;
1007
1016
  CREATE INDEX IF NOT EXISTS idx_tasks_task_type ON tasks(task_type);
1017
+ ALTER TABLE tasks ADD COLUMN cost_tokens INTEGER DEFAULT 0;
1018
+ ALTER TABLE tasks ADD COLUMN cost_usd REAL DEFAULT 0;
1019
+ ALTER TABLE tasks ADD COLUMN delegated_from TEXT;
1020
+ ALTER TABLE tasks ADD COLUMN delegation_depth INTEGER DEFAULT 0;
1021
+ ALTER TABLE tasks ADD COLUMN retry_count INTEGER DEFAULT 0;
1022
+ ALTER TABLE tasks ADD COLUMN max_retries INTEGER DEFAULT 3;
1023
+ ALTER TABLE tasks ADD COLUMN retry_after TEXT;
1024
+ ALTER TABLE tasks ADD COLUMN sla_minutes INTEGER;
1025
+
1026
+ CREATE TABLE IF NOT EXISTS task_traces (
1027
+ id TEXT PRIMARY KEY,
1028
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
1029
+ agent_id TEXT,
1030
+ trace_type TEXT NOT NULL CHECK(trace_type IN ('tool_call','llm_call','error','handoff','custom')),
1031
+ name TEXT,
1032
+ input_summary TEXT,
1033
+ output_summary TEXT,
1034
+ duration_ms INTEGER,
1035
+ tokens INTEGER,
1036
+ cost_usd REAL,
1037
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
1038
+ );
1039
+ CREATE INDEX IF NOT EXISTS idx_task_traces_task ON task_traces(task_id);
1040
+ CREATE INDEX IF NOT EXISTS idx_task_traces_agent ON task_traces(agent_id);
1041
+
1042
+ CREATE TABLE IF NOT EXISTS context_snapshots (
1043
+ id TEXT PRIMARY KEY,
1044
+ agent_id TEXT,
1045
+ task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
1046
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
1047
+ snapshot_type TEXT NOT NULL CHECK(snapshot_type IN ('interrupt','complete','handoff','checkpoint')),
1048
+ plan_summary TEXT,
1049
+ files_open TEXT DEFAULT '[]',
1050
+ attempts TEXT DEFAULT '[]',
1051
+ blockers TEXT DEFAULT '[]',
1052
+ next_steps TEXT,
1053
+ metadata TEXT DEFAULT '{}',
1054
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
1055
+ );
1056
+ CREATE INDEX IF NOT EXISTS idx_snapshots_agent ON context_snapshots(agent_id);
1057
+ CREATE INDEX IF NOT EXISTS idx_snapshots_task ON context_snapshots(task_id);
1058
+
1059
+ CREATE TABLE IF NOT EXISTS agent_budgets (
1060
+ agent_id TEXT PRIMARY KEY,
1061
+ max_concurrent INTEGER DEFAULT 5,
1062
+ max_cost_usd REAL,
1063
+ max_task_minutes INTEGER,
1064
+ period_hours INTEGER DEFAULT 24,
1065
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1066
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1067
+ );
1068
+
1008
1069
  INSERT OR IGNORE INTO _migrations (id) VALUES (35);
1009
1070
  `
1010
1071
  ];
@@ -1601,12 +1662,14 @@ var exports_tasks = {};
1601
1662
  __export(exports_tasks, {
1602
1663
  updateTask: () => updateTask,
1603
1664
  unlockTask: () => unlockTask,
1665
+ stealTask: () => stealTask,
1604
1666
  startTask: () => startTask,
1605
1667
  setTaskStatus: () => setTaskStatus,
1606
1668
  setTaskPriority: () => setTaskPriority,
1607
1669
  removeDependency: () => removeDependency,
1608
1670
  redistributeStaleTasks: () => redistributeStaleTasks,
1609
1671
  moveTask: () => moveTask,
1672
+ logCost: () => logCost,
1610
1673
  lockTask: () => lockTask,
1611
1674
  listTasks: () => listTasks,
1612
1675
  getTasksChangedSince: () => getTasksChangedSince,
@@ -1629,6 +1692,7 @@ __export(exports_tasks, {
1629
1692
  countTasks: () => countTasks,
1630
1693
  completeTask: () => completeTask,
1631
1694
  cloneTask: () => cloneTask,
1695
+ claimOrSteal: () => claimOrSteal,
1632
1696
  claimNextTask: () => claimNextTask,
1633
1697
  bulkUpdateTasks: () => bulkUpdateTasks,
1634
1698
  bulkCreateTasks: () => bulkCreateTasks,
@@ -2110,6 +2174,20 @@ function completeTask(id, agentId, db, options) {
2110
2174
  if (spawnedFromTemplate) {
2111
2175
  meta._spawned_task = { id: spawnedFromTemplate.id, short_id: spawnedFromTemplate.short_id, title: spawnedFromTemplate.title };
2112
2176
  }
2177
+ const unblockedDeps = d.query(`SELECT DISTINCT t.id, t.short_id, t.title FROM tasks t
2178
+ JOIN task_dependencies td ON td.task_id = t.id
2179
+ WHERE td.depends_on = ? AND t.status = 'pending'
2180
+ AND NOT EXISTS (
2181
+ SELECT 1 FROM task_dependencies td2
2182
+ JOIN tasks dep2 ON dep2.id = td2.depends_on
2183
+ WHERE td2.task_id = t.id AND dep2.status NOT IN ('completed', 'cancelled') AND dep2.id != ?
2184
+ )`).all(id, id);
2185
+ if (unblockedDeps.length > 0) {
2186
+ meta._unblocked = unblockedDeps.map((d2) => ({ id: d2.id, short_id: d2.short_id, title: d2.title }));
2187
+ for (const dep of unblockedDeps) {
2188
+ dispatchWebhook("task.unblocked", { id: dep.id, unblocked_by: id, title: dep.title }, d).catch(() => {});
2189
+ }
2190
+ }
2113
2191
  return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: task.version + 1, updated_at: timestamp, metadata: meta };
2114
2192
  }
2115
2193
  function lockTask(id, agentId, db) {
@@ -2406,24 +2484,36 @@ function failTask(id, agentId, reason, options, db) {
2406
2484
  };
2407
2485
  let retryTask;
2408
2486
  if (options?.retry) {
2409
- let title = task.title;
2410
- if (task.short_id && title.startsWith(task.short_id + ": ")) {
2411
- title = title.slice(task.short_id.length + 2);
2487
+ const retryCount = (task.retry_count || 0) + 1;
2488
+ const maxRetries = task.max_retries || 3;
2489
+ if (retryCount > maxRetries) {
2490
+ d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [
2491
+ JSON.stringify({ ...meta, _retry_exhausted: { retry_count: retryCount - 1, max_retries: maxRetries } }),
2492
+ id
2493
+ ]);
2494
+ } else {
2495
+ const backoffMinutes = Math.pow(5, retryCount - 1);
2496
+ const retryAfter = options.retry_after || new Date(Date.now() + backoffMinutes * 60 * 1000).toISOString();
2497
+ let title = task.title;
2498
+ if (task.short_id && title.startsWith(task.short_id + ": ")) {
2499
+ title = title.slice(task.short_id.length + 2);
2500
+ }
2501
+ retryTask = createTask({
2502
+ title,
2503
+ description: task.description ?? undefined,
2504
+ priority: task.priority,
2505
+ project_id: task.project_id ?? undefined,
2506
+ task_list_id: task.task_list_id ?? undefined,
2507
+ plan_id: task.plan_id ?? undefined,
2508
+ assigned_to: task.assigned_to ?? undefined,
2509
+ tags: task.tags,
2510
+ metadata: { ...task.metadata, _retry: { original_id: task.id, retry_count: retryCount, max_retries: maxRetries, retry_after: retryAfter, failure_reason: reason } },
2511
+ estimated_minutes: task.estimated_minutes ?? undefined,
2512
+ recurrence_rule: task.recurrence_rule ?? undefined,
2513
+ due_at: retryAfter
2514
+ }, d);
2515
+ d.run("UPDATE tasks SET retry_count = ?, max_retries = ?, retry_after = ? WHERE id = ?", [retryCount, maxRetries, retryAfter, retryTask.id]);
2412
2516
  }
2413
- retryTask = createTask({
2414
- title,
2415
- description: task.description ?? undefined,
2416
- priority: task.priority,
2417
- project_id: task.project_id ?? undefined,
2418
- task_list_id: task.task_list_id ?? undefined,
2419
- plan_id: task.plan_id ?? undefined,
2420
- assigned_to: task.assigned_to ?? undefined,
2421
- tags: task.tags,
2422
- metadata: { ...task.metadata, _retry: { original_id: task.id, retry_after: options.retry_after || null, failure_reason: reason } },
2423
- estimated_minutes: task.estimated_minutes ?? undefined,
2424
- recurrence_rule: task.recurrence_rule ?? undefined,
2425
- due_at: options.retry_after || task.due_at || undefined
2426
- }, d);
2427
2517
  }
2428
2518
  return { task: failedTask, retryTask };
2429
2519
  }
@@ -2447,6 +2537,40 @@ function getStaleTasks(staleMinutes = 30, filters, db) {
2447
2537
  const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at ASC`).all(...params);
2448
2538
  return rows.map(rowToTask);
2449
2539
  }
2540
+ function logCost(taskId, tokens, usd, db) {
2541
+ const d = db || getDatabase();
2542
+ d.run("UPDATE tasks SET cost_tokens = cost_tokens + ?, cost_usd = cost_usd + ?, updated_at = ? WHERE id = ?", [tokens, usd, now(), taskId]);
2543
+ }
2544
+ function stealTask(agentId, opts, db) {
2545
+ const d = db || getDatabase();
2546
+ const staleMinutes = opts?.stale_minutes ?? 30;
2547
+ const staleTasks = getStaleTasks(staleMinutes, { project_id: opts?.project_id, task_list_id: opts?.task_list_id }, d);
2548
+ if (staleTasks.length === 0)
2549
+ return null;
2550
+ const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
2551
+ staleTasks.sort((a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9));
2552
+ const target = staleTasks[0];
2553
+ const timestamp = now();
2554
+ d.run(`UPDATE tasks SET assigned_to = ?, locked_by = ?, locked_at = ?, updated_at = ?, version = version + 1 WHERE id = ?`, [agentId, agentId, timestamp, timestamp, target.id]);
2555
+ logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
2556
+ dispatchWebhook("task.assigned", { id: target.id, agent_id: agentId, title: target.title, stolen_from: target.assigned_to }, d).catch(() => {});
2557
+ return { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
2558
+ }
2559
+ function claimOrSteal(agentId, filters, db) {
2560
+ const d = db || getDatabase();
2561
+ const tx = d.transaction(() => {
2562
+ const next = getNextTask(agentId, filters, d);
2563
+ if (next) {
2564
+ const started = startTask(next.id, agentId, d);
2565
+ return { task: started, stolen: false };
2566
+ }
2567
+ const stolen = stealTask(agentId, { stale_minutes: filters?.stale_minutes, project_id: filters?.project_id, task_list_id: filters?.task_list_id }, d);
2568
+ if (stolen)
2569
+ return { task: stolen, stolen: true };
2570
+ return null;
2571
+ });
2572
+ return tx();
2573
+ }
2450
2574
  function getStatus(filters, agentId, options, db) {
2451
2575
  const d = db || getDatabase();
2452
2576
  const pending = countTasks({ ...filters, status: "pending" }, d);
@@ -3042,6 +3166,219 @@ var init_task_commits = __esm(() => {
3042
3166
  init_database();
3043
3167
  });
3044
3168
 
3169
+ // src/db/traces.ts
3170
+ var exports_traces = {};
3171
+ __export(exports_traces, {
3172
+ logTrace: () => logTrace,
3173
+ getTraceStats: () => getTraceStats,
3174
+ getTaskTraces: () => getTaskTraces
3175
+ });
3176
+ function logTrace(input, db) {
3177
+ const d = db || getDatabase();
3178
+ const id = uuid();
3179
+ const timestamp = now();
3180
+ d.run(`INSERT INTO task_traces (id, task_id, agent_id, trace_type, name, input_summary, output_summary, duration_ms, tokens, cost_usd, created_at)
3181
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3182
+ id,
3183
+ input.task_id,
3184
+ input.agent_id || null,
3185
+ input.trace_type,
3186
+ input.name || null,
3187
+ input.input_summary || null,
3188
+ input.output_summary || null,
3189
+ input.duration_ms ?? null,
3190
+ input.tokens ?? null,
3191
+ input.cost_usd ?? null,
3192
+ timestamp
3193
+ ]);
3194
+ return {
3195
+ id,
3196
+ task_id: input.task_id,
3197
+ agent_id: input.agent_id || null,
3198
+ trace_type: input.trace_type,
3199
+ name: input.name || null,
3200
+ input_summary: input.input_summary || null,
3201
+ output_summary: input.output_summary || null,
3202
+ duration_ms: input.duration_ms ?? null,
3203
+ tokens: input.tokens ?? null,
3204
+ cost_usd: input.cost_usd ?? null,
3205
+ created_at: timestamp
3206
+ };
3207
+ }
3208
+ function getTaskTraces(taskId, db) {
3209
+ const d = db || getDatabase();
3210
+ return d.query("SELECT * FROM task_traces WHERE task_id = ? ORDER BY created_at DESC").all(taskId);
3211
+ }
3212
+ function getTraceStats(taskId, db) {
3213
+ const d = db || getDatabase();
3214
+ const row = d.query(`SELECT COUNT(*) as total,
3215
+ SUM(CASE WHEN trace_type = 'tool_call' THEN 1 ELSE 0 END) as tool_calls,
3216
+ SUM(CASE WHEN trace_type = 'llm_call' THEN 1 ELSE 0 END) as llm_calls,
3217
+ SUM(CASE WHEN trace_type = 'error' THEN 1 ELSE 0 END) as errors,
3218
+ COALESCE(SUM(tokens), 0) as total_tokens,
3219
+ COALESCE(SUM(cost_usd), 0) as total_cost_usd,
3220
+ COALESCE(SUM(duration_ms), 0) as total_duration_ms
3221
+ FROM task_traces WHERE task_id = ?`).get(taskId);
3222
+ return row;
3223
+ }
3224
+ var init_traces = __esm(() => {
3225
+ init_database();
3226
+ });
3227
+
3228
+ // src/db/snapshots.ts
3229
+ var exports_snapshots = {};
3230
+ __export(exports_snapshots, {
3231
+ saveSnapshot: () => saveSnapshot,
3232
+ listSnapshots: () => listSnapshots,
3233
+ getLatestSnapshot: () => getLatestSnapshot
3234
+ });
3235
+ function rowToSnapshot(row) {
3236
+ return {
3237
+ ...row,
3238
+ snapshot_type: row.snapshot_type,
3239
+ files_open: JSON.parse(row.files_open || "[]"),
3240
+ attempts: JSON.parse(row.attempts || "[]"),
3241
+ blockers: JSON.parse(row.blockers || "[]"),
3242
+ metadata: JSON.parse(row.metadata || "{}")
3243
+ };
3244
+ }
3245
+ function saveSnapshot(input, db) {
3246
+ const d = db || getDatabase();
3247
+ const id = uuid();
3248
+ const timestamp = now();
3249
+ d.run(`INSERT INTO context_snapshots (id, agent_id, task_id, project_id, snapshot_type, plan_summary, files_open, attempts, blockers, next_steps, metadata, created_at)
3250
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3251
+ id,
3252
+ input.agent_id || null,
3253
+ input.task_id || null,
3254
+ input.project_id || null,
3255
+ input.snapshot_type,
3256
+ input.plan_summary || null,
3257
+ JSON.stringify(input.files_open || []),
3258
+ JSON.stringify(input.attempts || []),
3259
+ JSON.stringify(input.blockers || []),
3260
+ input.next_steps || null,
3261
+ JSON.stringify(input.metadata || {}),
3262
+ timestamp
3263
+ ]);
3264
+ return {
3265
+ id,
3266
+ agent_id: input.agent_id || null,
3267
+ task_id: input.task_id || null,
3268
+ project_id: input.project_id || null,
3269
+ snapshot_type: input.snapshot_type,
3270
+ plan_summary: input.plan_summary || null,
3271
+ files_open: input.files_open || [],
3272
+ attempts: input.attempts || [],
3273
+ blockers: input.blockers || [],
3274
+ next_steps: input.next_steps || null,
3275
+ metadata: input.metadata || {},
3276
+ created_at: timestamp
3277
+ };
3278
+ }
3279
+ function getLatestSnapshot(agentId, taskId, db) {
3280
+ const d = db || getDatabase();
3281
+ const conditions = [];
3282
+ const params = [];
3283
+ if (agentId) {
3284
+ conditions.push("agent_id = ?");
3285
+ params.push(agentId);
3286
+ }
3287
+ if (taskId) {
3288
+ conditions.push("task_id = ?");
3289
+ params.push(taskId);
3290
+ }
3291
+ if (conditions.length === 0)
3292
+ return null;
3293
+ const where = conditions.join(" AND ");
3294
+ const row = d.query(`SELECT * FROM context_snapshots WHERE ${where} ORDER BY created_at DESC LIMIT 1`).get(...params);
3295
+ return row ? rowToSnapshot(row) : null;
3296
+ }
3297
+ function listSnapshots(opts, db) {
3298
+ const d = db || getDatabase();
3299
+ const conditions = [];
3300
+ const params = [];
3301
+ if (opts.agent_id) {
3302
+ conditions.push("agent_id = ?");
3303
+ params.push(opts.agent_id);
3304
+ }
3305
+ if (opts.task_id) {
3306
+ conditions.push("task_id = ?");
3307
+ params.push(opts.task_id);
3308
+ }
3309
+ if (opts.project_id) {
3310
+ conditions.push("project_id = ?");
3311
+ params.push(opts.project_id);
3312
+ }
3313
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
3314
+ const limit = opts.limit || 20;
3315
+ params.push(limit);
3316
+ return d.query(`SELECT * FROM context_snapshots ${where} ORDER BY created_at DESC LIMIT ?`).all(...params).map(rowToSnapshot);
3317
+ }
3318
+ var init_snapshots = __esm(() => {
3319
+ init_database();
3320
+ });
3321
+
3322
+ // src/db/budgets.ts
3323
+ var exports_budgets = {};
3324
+ __export(exports_budgets, {
3325
+ setBudget: () => setBudget,
3326
+ getBudget: () => getBudget,
3327
+ checkBudget: () => checkBudget
3328
+ });
3329
+ function setBudget(agentId, opts, db) {
3330
+ const d = db || getDatabase();
3331
+ const timestamp = now();
3332
+ d.run(`INSERT INTO agent_budgets (agent_id, max_concurrent, max_cost_usd, max_task_minutes, period_hours, created_at, updated_at)
3333
+ VALUES (?, ?, ?, ?, ?, ?, ?)
3334
+ ON CONFLICT(agent_id) DO UPDATE SET
3335
+ max_concurrent = COALESCE(?, max_concurrent),
3336
+ max_cost_usd = COALESCE(?, max_cost_usd),
3337
+ max_task_minutes = COALESCE(?, max_task_minutes),
3338
+ period_hours = COALESCE(?, period_hours),
3339
+ updated_at = ?`, [
3340
+ agentId,
3341
+ opts.max_concurrent ?? 5,
3342
+ opts.max_cost_usd ?? null,
3343
+ opts.max_task_minutes ?? null,
3344
+ opts.period_hours ?? 24,
3345
+ timestamp,
3346
+ timestamp,
3347
+ opts.max_concurrent ?? null,
3348
+ opts.max_cost_usd ?? null,
3349
+ opts.max_task_minutes ?? null,
3350
+ opts.period_hours ?? null,
3351
+ timestamp
3352
+ ]);
3353
+ return getBudget(agentId, d);
3354
+ }
3355
+ function getBudget(agentId, db) {
3356
+ const d = db || getDatabase();
3357
+ return d.query("SELECT * FROM agent_budgets WHERE agent_id = ?").get(agentId);
3358
+ }
3359
+ function checkBudget(agentId, db) {
3360
+ const d = db || getDatabase();
3361
+ const budget = getBudget(agentId, d);
3362
+ if (!budget)
3363
+ return { allowed: true, current_concurrent: 0, max_concurrent: 999 };
3364
+ const concurrent = countTasks({ status: "in_progress", assigned_to: agentId }, d);
3365
+ if (concurrent >= budget.max_concurrent) {
3366
+ return { allowed: false, reason: `Concurrent limit reached (${concurrent}/${budget.max_concurrent})`, current_concurrent: concurrent, max_concurrent: budget.max_concurrent };
3367
+ }
3368
+ if (budget.max_cost_usd != null) {
3369
+ const periodStart = new Date(Date.now() - budget.period_hours * 60 * 60 * 1000).toISOString();
3370
+ const costRow = d.query("SELECT COALESCE(SUM(cost_usd), 0) as total FROM tasks WHERE (assigned_to = ? OR agent_id = ?) AND updated_at > ?").get(agentId, agentId, periodStart);
3371
+ if (costRow.total >= budget.max_cost_usd) {
3372
+ return { allowed: false, reason: `Cost limit reached ($${costRow.total.toFixed(2)}/$${budget.max_cost_usd.toFixed(2)} in ${budget.period_hours}h)`, current_concurrent: concurrent, max_concurrent: budget.max_concurrent, current_cost_usd: costRow.total, max_cost_usd: budget.max_cost_usd };
3373
+ }
3374
+ }
3375
+ return { allowed: true, current_concurrent: concurrent, max_concurrent: budget.max_concurrent };
3376
+ }
3377
+ var init_budgets = __esm(() => {
3378
+ init_database();
3379
+ init_tasks();
3380
+ });
3381
+
3045
3382
  // src/lib/github.ts
3046
3383
  var exports_github = {};
3047
3384
  __export(exports_github, {
@@ -9398,7 +9735,23 @@ function formatError(error) {
9398
9735
  return JSON.stringify({ code: CompletionGuardError.code, message: error.reason, suggestion: CompletionGuardError.suggestion, ...retry });
9399
9736
  }
9400
9737
  if (error instanceof Error) {
9401
- return JSON.stringify({ code: "UNKNOWN_ERROR", message: error.message });
9738
+ const msg = error.message;
9739
+ if (msg.includes("UNIQUE constraint failed: projects.path")) {
9740
+ const db = getDatabase();
9741
+ const existing = db.prepare("SELECT id, name FROM projects WHERE path = ?").get(msg.match(/'([^']+)'$/)?.[1] ?? "");
9742
+ return JSON.stringify({ code: "DUPLICATE_PROJECT", message: `Project already exists at this path${existing ? ` (id: ${existing.id}, name: ${existing.name})` : ""}. Use list_projects to find it.`, suggestion: "Use list_projects or get_project to retrieve the existing project." });
9743
+ }
9744
+ if (msg.includes("UNIQUE constraint failed: projects.name")) {
9745
+ return JSON.stringify({ code: "DUPLICATE_PROJECT", message: "A project with this name already exists. Use a different name or list_projects to find the existing one.", suggestion: "Use list_projects to see existing projects." });
9746
+ }
9747
+ if (msg.includes("UNIQUE constraint failed")) {
9748
+ const table = msg.match(/UNIQUE constraint failed: (\w+)\./)?.[1] ?? "unknown";
9749
+ return JSON.stringify({ code: "DUPLICATE_ENTRY", message: `Duplicate entry in ${table}. The record already exists.`, suggestion: `Use the list or get endpoint for ${table} to find the existing record.` });
9750
+ }
9751
+ if (msg.includes("FOREIGN KEY constraint failed")) {
9752
+ return JSON.stringify({ code: "REFERENCE_ERROR", message: "Referenced record does not exist. Check that the ID is correct.", suggestion: "Verify the referenced ID exists before creating this record." });
9753
+ }
9754
+ return JSON.stringify({ code: "UNKNOWN_ERROR", message: msg });
9402
9755
  }
9403
9756
  return JSON.stringify({ code: "UNKNOWN_ERROR", message: String(error) });
9404
9757
  }
@@ -10849,6 +11202,135 @@ Blocked:`);
10849
11202
  }
10850
11203
  });
10851
11204
  }
11205
+ if (shouldRegisterTool("log_cost")) {
11206
+ server.tool("log_cost", "Log token usage and cost to a task. Accumulates \u2014 call after each LLM invocation.", { task_id: exports_external.string(), tokens: exports_external.number().describe("Token count"), usd: exports_external.number().describe("Cost in USD") }, async ({ task_id, tokens, usd }) => {
11207
+ try {
11208
+ const { logCost: logCost2 } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
11209
+ const resolvedId = resolveId(task_id, "tasks");
11210
+ logCost2(resolvedId, tokens, usd);
11211
+ return { content: [{ type: "text", text: `Logged ${tokens} tokens ($${usd.toFixed(4)}) to ${task_id}` }] };
11212
+ } catch (e) {
11213
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
11214
+ }
11215
+ });
11216
+ }
11217
+ if (shouldRegisterTool("log_trace")) {
11218
+ server.tool("log_trace", "Log a trace entry (tool call, LLM call, error, handoff) to a task for observability.", {
11219
+ task_id: exports_external.string(),
11220
+ agent_id: exports_external.string().optional(),
11221
+ trace_type: exports_external.enum(["tool_call", "llm_call", "error", "handoff", "custom"]),
11222
+ name: exports_external.string().optional(),
11223
+ input_summary: exports_external.string().optional(),
11224
+ output_summary: exports_external.string().optional(),
11225
+ duration_ms: exports_external.number().optional(),
11226
+ tokens: exports_external.number().optional(),
11227
+ cost_usd: exports_external.number().optional()
11228
+ }, async ({ task_id, agent_id, trace_type, name, input_summary, output_summary, duration_ms, tokens, cost_usd }) => {
11229
+ try {
11230
+ const { logTrace: logTrace2 } = await Promise.resolve().then(() => (init_traces(), exports_traces));
11231
+ const resolvedId = resolveId(task_id, "tasks");
11232
+ const trace = logTrace2({ task_id: resolvedId, agent_id, trace_type, name, input_summary, output_summary, duration_ms, tokens, cost_usd });
11233
+ return { content: [{ type: "text", text: `Trace logged: ${trace.id} [${trace_type}] ${name || ""}` }] };
11234
+ } catch (e) {
11235
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
11236
+ }
11237
+ });
11238
+ }
11239
+ if (shouldRegisterTool("get_traces")) {
11240
+ server.tool("get_traces", "Get execution traces for a task \u2014 tool calls, LLM invocations, errors, handoffs.", { task_id: exports_external.string() }, async ({ task_id }) => {
11241
+ try {
11242
+ const { getTaskTraces: getTaskTraces2, getTraceStats: getTraceStats2 } = await Promise.resolve().then(() => (init_traces(), exports_traces));
11243
+ const resolvedId = resolveId(task_id, "tasks");
11244
+ const traces = getTaskTraces2(resolvedId);
11245
+ const stats = getTraceStats2(resolvedId);
11246
+ const lines = [`Traces for ${task_id}: ${stats.total} total (${stats.tool_calls} tools, ${stats.llm_calls} LLM, ${stats.errors} errors) | ${stats.total_tokens} tokens | $${stats.total_cost_usd.toFixed(4)} | ${stats.total_duration_ms}ms`];
11247
+ for (const t of traces.slice(0, 20)) {
11248
+ lines.push(` ${t.created_at} [${t.trace_type}] ${t.name || ""} ${t.tokens ? t.tokens + "tok" : ""} ${t.duration_ms ? t.duration_ms + "ms" : ""}`);
11249
+ }
11250
+ return { content: [{ type: "text", text: lines.join(`
11251
+ `) }] };
11252
+ } catch (e) {
11253
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
11254
+ }
11255
+ });
11256
+ }
11257
+ if (shouldRegisterTool("save_snapshot")) {
11258
+ server.tool("save_snapshot", "Save a structured context snapshot \u2014 what you were working on, what files are open, what was tried, blockers, next steps. Call on session end or before handoff.", {
11259
+ agent_id: exports_external.string().optional(),
11260
+ task_id: exports_external.string().optional(),
11261
+ project_id: exports_external.string().optional(),
11262
+ snapshot_type: exports_external.enum(["interrupt", "complete", "handoff", "checkpoint"]),
11263
+ plan_summary: exports_external.string().optional(),
11264
+ files_open: exports_external.array(exports_external.string()).optional(),
11265
+ attempts: exports_external.array(exports_external.string()).optional(),
11266
+ blockers: exports_external.array(exports_external.string()).optional(),
11267
+ next_steps: exports_external.string().optional()
11268
+ }, async ({ agent_id, task_id, project_id, snapshot_type, plan_summary, files_open, attempts, blockers, next_steps }) => {
11269
+ try {
11270
+ const { saveSnapshot: saveSnapshot2 } = await Promise.resolve().then(() => (init_snapshots(), exports_snapshots));
11271
+ const snap = saveSnapshot2({ agent_id, task_id, project_id, snapshot_type, plan_summary, files_open, attempts, blockers, next_steps });
11272
+ return { content: [{ type: "text", text: `Snapshot saved: ${snap.id} [${snapshot_type}]${plan_summary ? " \u2014 " + plan_summary.slice(0, 80) : ""}` }] };
11273
+ } catch (e) {
11274
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
11275
+ }
11276
+ });
11277
+ }
11278
+ if (shouldRegisterTool("get_snapshot")) {
11279
+ server.tool("get_snapshot", "Get the latest context snapshot for an agent or task \u2014 use to resume work after interruption.", { agent_id: exports_external.string().optional(), task_id: exports_external.string().optional() }, async ({ agent_id, task_id }) => {
11280
+ try {
11281
+ const { getLatestSnapshot: getLatestSnapshot2 } = await Promise.resolve().then(() => (init_snapshots(), exports_snapshots));
11282
+ const snap = getLatestSnapshot2(agent_id, task_id);
11283
+ if (!snap)
11284
+ return { content: [{ type: "text", text: "No snapshot found." }] };
11285
+ const lines = [`Snapshot [${snap.snapshot_type}] from ${snap.created_at}`];
11286
+ if (snap.plan_summary)
11287
+ lines.push(`Plan: ${snap.plan_summary}`);
11288
+ if (snap.files_open.length > 0)
11289
+ lines.push(`Files: ${snap.files_open.join(", ")}`);
11290
+ if (snap.blockers.length > 0)
11291
+ lines.push(`Blockers: ${snap.blockers.join(", ")}`);
11292
+ if (snap.next_steps)
11293
+ lines.push(`Next: ${snap.next_steps}`);
11294
+ if (snap.attempts.length > 0)
11295
+ lines.push(`Attempts: ${snap.attempts.join("; ")}`);
11296
+ return { content: [{ type: "text", text: lines.join(`
11297
+ `) }] };
11298
+ } catch (e) {
11299
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
11300
+ }
11301
+ });
11302
+ }
11303
+ if (shouldRegisterTool("set_budget")) {
11304
+ server.tool("set_budget", "Set execution budget for an agent \u2014 max concurrent tasks, cost limit, time limit per period.", {
11305
+ agent_id: exports_external.string(),
11306
+ max_concurrent: exports_external.number().optional(),
11307
+ max_cost_usd: exports_external.number().optional(),
11308
+ max_task_minutes: exports_external.number().optional(),
11309
+ period_hours: exports_external.number().optional()
11310
+ }, async ({ agent_id, max_concurrent, max_cost_usd, max_task_minutes, period_hours }) => {
11311
+ try {
11312
+ const { setBudget: setBudget2 } = await Promise.resolve().then(() => (init_budgets(), exports_budgets));
11313
+ const budget = setBudget2(agent_id, { max_concurrent, max_cost_usd, max_task_minutes, period_hours });
11314
+ return { content: [{ type: "text", text: `Budget set for ${agent_id}: max ${budget.max_concurrent} concurrent, $${budget.max_cost_usd ?? "\u221E"} cost limit, ${budget.period_hours}h period` }] };
11315
+ } catch (e) {
11316
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
11317
+ }
11318
+ });
11319
+ }
11320
+ if (shouldRegisterTool("check_budget")) {
11321
+ server.tool("check_budget", "Check if an agent is within their execution budget (concurrent tasks, cost, time).", { agent_id: exports_external.string() }, async ({ agent_id }) => {
11322
+ try {
11323
+ const { checkBudget: checkBudget2 } = await Promise.resolve().then(() => (init_budgets(), exports_budgets));
11324
+ const result = checkBudget2(agent_id);
11325
+ if (result.allowed) {
11326
+ return { content: [{ type: "text", text: `Budget OK: ${result.current_concurrent}/${result.max_concurrent} concurrent tasks` }] };
11327
+ }
11328
+ return { content: [{ type: "text", text: `BUDGET EXCEEDED: ${result.reason}` }], isError: true };
11329
+ } catch (e) {
11330
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
11331
+ }
11332
+ });
11333
+ }
10852
11334
  if (shouldRegisterTool("import_github_issue")) {
10853
11335
  server.tool("import_github_issue", "Import a GitHub issue as a task. Requires gh CLI installed and authenticated.", {
10854
11336
  url: exports_external.string().describe("GitHub issue URL (e.g. https://github.com/owner/repo/issues/42)"),
@@ -11559,6 +12041,56 @@ ${formatTaskDetail(task, 300)}` }] };
11559
12041
  }
11560
12042
  });
11561
12043
  }
12044
+ if (shouldRegisterTool("steal_task")) {
12045
+ server.tool("steal_task", "Work-stealing: take the highest-priority stale in_progress task from another agent and reassign it to you.", {
12046
+ agent_id: exports_external.string().describe("Your agent ID"),
12047
+ stale_minutes: exports_external.number().optional().describe("How long a task must be stale before stealing (default: 30)"),
12048
+ project_id: exports_external.string().optional(),
12049
+ task_list_id: exports_external.string().optional()
12050
+ }, async ({ agent_id, stale_minutes, project_id, task_list_id }) => {
12051
+ try {
12052
+ const task = stealTask(agent_id, { stale_minutes, project_id, task_list_id });
12053
+ if (!task)
12054
+ return { content: [{ type: "text", text: "No stale tasks available to steal." }] };
12055
+ return { content: [{ type: "text", text: `Stolen: ${formatTask(task)}
12056
+ Previous owner: ${task.metadata?._stolen_from || "unknown"}` }] };
12057
+ } catch (e) {
12058
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
12059
+ }
12060
+ });
12061
+ }
12062
+ if (shouldRegisterTool("claim_or_steal")) {
12063
+ server.tool("claim_or_steal", "Try to claim a pending task first; if none available, steal from a stale agent. Best single call for getting work.", {
12064
+ agent_id: exports_external.string(),
12065
+ project_id: exports_external.string().optional(),
12066
+ task_list_id: exports_external.string().optional(),
12067
+ plan_id: exports_external.string().optional(),
12068
+ tags: exports_external.array(exports_external.string()).optional(),
12069
+ stale_minutes: exports_external.number().optional().describe("Stale threshold for work-stealing fallback (default: 30)")
12070
+ }, async ({ agent_id, project_id, task_list_id, plan_id, tags, stale_minutes }) => {
12071
+ try {
12072
+ const filters = {};
12073
+ if (project_id)
12074
+ filters.project_id = resolveId(project_id, "projects");
12075
+ if (task_list_id)
12076
+ filters.task_list_id = resolveId(task_list_id, "task_lists");
12077
+ if (plan_id)
12078
+ filters.plan_id = resolveId(plan_id, "plans");
12079
+ if (tags)
12080
+ filters.tags = tags;
12081
+ if (stale_minutes)
12082
+ filters.stale_minutes = stale_minutes;
12083
+ const result = claimOrSteal(agent_id, Object.keys(filters).length > 0 ? filters : undefined);
12084
+ if (!result)
12085
+ return { content: [{ type: "text", text: "No tasks available to claim or steal." }] };
12086
+ const prefix = result.stolen ? "Stolen" : "Claimed";
12087
+ return { content: [{ type: "text", text: `${prefix}: ${formatTask(result.task)}
12088
+ ${formatTaskDetail(result.task, 300)}` }] };
12089
+ } catch (e) {
12090
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
12091
+ }
12092
+ });
12093
+ }
11562
12094
  if (shouldRegisterTool("get_stale_tasks")) {
11563
12095
  server.tool("get_stale_tasks", "Find stale in_progress tasks with no recent activity.", {
11564
12096
  stale_minutes: exports_external.number().optional(),