@hasna/todos 0.10.21 → 0.11.1

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/cli/index.js CHANGED
@@ -6,60 +6,39 @@ var __defProp = Object.defineProperty;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
- function __accessProp(key) {
10
- return this[key];
11
- }
12
- var __toESMCache_node;
13
- var __toESMCache_esm;
14
9
  var __toESM = (mod, isNodeMode, target) => {
15
- var canCache = mod != null && typeof mod === "object";
16
- if (canCache) {
17
- var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
18
- var cached = cache.get(mod);
19
- if (cached)
20
- return cached;
21
- }
22
10
  target = mod != null ? __create(__getProtoOf(mod)) : {};
23
11
  const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
24
12
  for (let key of __getOwnPropNames(mod))
25
13
  if (!__hasOwnProp.call(to, key))
26
14
  __defProp(to, key, {
27
- get: __accessProp.bind(mod, key),
15
+ get: () => mod[key],
28
16
  enumerable: true
29
17
  });
30
- if (canCache)
31
- cache.set(mod, to);
32
18
  return to;
33
19
  };
20
+ var __moduleCache = /* @__PURE__ */ new WeakMap;
34
21
  var __toCommonJS = (from) => {
35
- var entry = (__moduleCache ??= new WeakMap).get(from), desc;
22
+ var entry = __moduleCache.get(from), desc;
36
23
  if (entry)
37
24
  return entry;
38
25
  entry = __defProp({}, "__esModule", { value: true });
39
- if (from && typeof from === "object" || typeof from === "function") {
40
- for (var key of __getOwnPropNames(from))
41
- if (!__hasOwnProp.call(entry, key))
42
- __defProp(entry, key, {
43
- get: __accessProp.bind(from, key),
44
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
45
- });
46
- }
26
+ if (from && typeof from === "object" || typeof from === "function")
27
+ __getOwnPropNames(from).map((key) => !__hasOwnProp.call(entry, key) && __defProp(entry, key, {
28
+ get: () => from[key],
29
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
30
+ }));
47
31
  __moduleCache.set(from, entry);
48
32
  return entry;
49
33
  };
50
- var __moduleCache;
51
34
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
52
- var __returnValue = (v) => v;
53
- function __exportSetter(name, newValue) {
54
- this[name] = __returnValue.bind(null, newValue);
55
- }
56
35
  var __export = (target, all) => {
57
36
  for (var name in all)
58
37
  __defProp(target, name, {
59
38
  get: all[name],
60
39
  enumerable: true,
61
40
  configurable: true,
62
- set: __exportSetter.bind(all, name)
41
+ set: (newValue) => all[name] = () => newValue
63
42
  });
64
43
  };
65
44
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -2347,6 +2326,15 @@ function ensureSchema(db) {
2347
2326
  ensureColumn("tasks", "assigned_by", "TEXT");
2348
2327
  ensureColumn("tasks", "assigned_from_project", "TEXT");
2349
2328
  ensureColumn("tasks", "started_at", "TEXT");
2329
+ ensureColumn("tasks", "task_type", "TEXT");
2330
+ ensureColumn("tasks", "cost_tokens", "INTEGER DEFAULT 0");
2331
+ ensureColumn("tasks", "cost_usd", "REAL DEFAULT 0");
2332
+ ensureColumn("tasks", "delegated_from", "TEXT");
2333
+ ensureColumn("tasks", "delegation_depth", "INTEGER DEFAULT 0");
2334
+ ensureColumn("tasks", "retry_count", "INTEGER DEFAULT 0");
2335
+ ensureColumn("tasks", "max_retries", "INTEGER DEFAULT 3");
2336
+ ensureColumn("tasks", "retry_after", "TEXT");
2337
+ ensureColumn("tasks", "sla_minutes", "INTEGER");
2350
2338
  ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
2351
2339
  ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
2352
2340
  ensureColumn("agents", "reports_to", "TEXT");
@@ -2960,6 +2948,58 @@ var init_database = __esm(() => {
2960
2948
  `
2961
2949
  ALTER TABLE tasks ADD COLUMN task_type TEXT;
2962
2950
  CREATE INDEX IF NOT EXISTS idx_tasks_task_type ON tasks(task_type);
2951
+ ALTER TABLE tasks ADD COLUMN cost_tokens INTEGER DEFAULT 0;
2952
+ ALTER TABLE tasks ADD COLUMN cost_usd REAL DEFAULT 0;
2953
+ ALTER TABLE tasks ADD COLUMN delegated_from TEXT;
2954
+ ALTER TABLE tasks ADD COLUMN delegation_depth INTEGER DEFAULT 0;
2955
+ ALTER TABLE tasks ADD COLUMN retry_count INTEGER DEFAULT 0;
2956
+ ALTER TABLE tasks ADD COLUMN max_retries INTEGER DEFAULT 3;
2957
+ ALTER TABLE tasks ADD COLUMN retry_after TEXT;
2958
+ ALTER TABLE tasks ADD COLUMN sla_minutes INTEGER;
2959
+
2960
+ CREATE TABLE IF NOT EXISTS task_traces (
2961
+ id TEXT PRIMARY KEY,
2962
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
2963
+ agent_id TEXT,
2964
+ trace_type TEXT NOT NULL CHECK(trace_type IN ('tool_call','llm_call','error','handoff','custom')),
2965
+ name TEXT,
2966
+ input_summary TEXT,
2967
+ output_summary TEXT,
2968
+ duration_ms INTEGER,
2969
+ tokens INTEGER,
2970
+ cost_usd REAL,
2971
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
2972
+ );
2973
+ CREATE INDEX IF NOT EXISTS idx_task_traces_task ON task_traces(task_id);
2974
+ CREATE INDEX IF NOT EXISTS idx_task_traces_agent ON task_traces(agent_id);
2975
+
2976
+ CREATE TABLE IF NOT EXISTS context_snapshots (
2977
+ id TEXT PRIMARY KEY,
2978
+ agent_id TEXT,
2979
+ task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
2980
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
2981
+ snapshot_type TEXT NOT NULL CHECK(snapshot_type IN ('interrupt','complete','handoff','checkpoint')),
2982
+ plan_summary TEXT,
2983
+ files_open TEXT DEFAULT '[]',
2984
+ attempts TEXT DEFAULT '[]',
2985
+ blockers TEXT DEFAULT '[]',
2986
+ next_steps TEXT,
2987
+ metadata TEXT DEFAULT '{}',
2988
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
2989
+ );
2990
+ CREATE INDEX IF NOT EXISTS idx_snapshots_agent ON context_snapshots(agent_id);
2991
+ CREATE INDEX IF NOT EXISTS idx_snapshots_task ON context_snapshots(task_id);
2992
+
2993
+ CREATE TABLE IF NOT EXISTS agent_budgets (
2994
+ agent_id TEXT PRIMARY KEY,
2995
+ max_concurrent INTEGER DEFAULT 5,
2996
+ max_cost_usd REAL,
2997
+ max_task_minutes INTEGER,
2998
+ period_hours INTEGER DEFAULT 24,
2999
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
3000
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
3001
+ );
3002
+
2963
3003
  INSERT OR IGNORE INTO _migrations (id) VALUES (35);
2964
3004
  `
2965
3005
  ];
@@ -3755,12 +3795,14 @@ var exports_tasks = {};
3755
3795
  __export(exports_tasks, {
3756
3796
  updateTask: () => updateTask,
3757
3797
  unlockTask: () => unlockTask,
3798
+ stealTask: () => stealTask,
3758
3799
  startTask: () => startTask,
3759
3800
  setTaskStatus: () => setTaskStatus,
3760
3801
  setTaskPriority: () => setTaskPriority,
3761
3802
  removeDependency: () => removeDependency,
3762
3803
  redistributeStaleTasks: () => redistributeStaleTasks,
3763
3804
  moveTask: () => moveTask,
3805
+ logCost: () => logCost,
3764
3806
  lockTask: () => lockTask,
3765
3807
  listTasks: () => listTasks,
3766
3808
  getTasksChangedSince: () => getTasksChangedSince,
@@ -3783,6 +3825,7 @@ __export(exports_tasks, {
3783
3825
  countTasks: () => countTasks,
3784
3826
  completeTask: () => completeTask,
3785
3827
  cloneTask: () => cloneTask,
3828
+ claimOrSteal: () => claimOrSteal,
3786
3829
  claimNextTask: () => claimNextTask,
3787
3830
  bulkUpdateTasks: () => bulkUpdateTasks,
3788
3831
  bulkCreateTasks: () => bulkCreateTasks,
@@ -4264,6 +4307,20 @@ function completeTask(id, agentId, db, options) {
4264
4307
  if (spawnedFromTemplate) {
4265
4308
  meta._spawned_task = { id: spawnedFromTemplate.id, short_id: spawnedFromTemplate.short_id, title: spawnedFromTemplate.title };
4266
4309
  }
4310
+ const unblockedDeps = d.query(`SELECT DISTINCT t.id, t.short_id, t.title FROM tasks t
4311
+ JOIN task_dependencies td ON td.task_id = t.id
4312
+ WHERE td.depends_on = ? AND t.status = 'pending'
4313
+ AND NOT EXISTS (
4314
+ SELECT 1 FROM task_dependencies td2
4315
+ JOIN tasks dep2 ON dep2.id = td2.depends_on
4316
+ WHERE td2.task_id = t.id AND dep2.status NOT IN ('completed', 'cancelled') AND dep2.id != ?
4317
+ )`).all(id, id);
4318
+ if (unblockedDeps.length > 0) {
4319
+ meta._unblocked = unblockedDeps.map((d2) => ({ id: d2.id, short_id: d2.short_id, title: d2.title }));
4320
+ for (const dep of unblockedDeps) {
4321
+ dispatchWebhook("task.unblocked", { id: dep.id, unblocked_by: id, title: dep.title }, d).catch(() => {});
4322
+ }
4323
+ }
4267
4324
  return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: task.version + 1, updated_at: timestamp, metadata: meta };
4268
4325
  }
4269
4326
  function lockTask(id, agentId, db) {
@@ -4560,24 +4617,36 @@ function failTask(id, agentId, reason, options, db) {
4560
4617
  };
4561
4618
  let retryTask;
4562
4619
  if (options?.retry) {
4563
- let title = task.title;
4564
- if (task.short_id && title.startsWith(task.short_id + ": ")) {
4565
- title = title.slice(task.short_id.length + 2);
4620
+ const retryCount = (task.retry_count || 0) + 1;
4621
+ const maxRetries = task.max_retries || 3;
4622
+ if (retryCount > maxRetries) {
4623
+ d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [
4624
+ JSON.stringify({ ...meta, _retry_exhausted: { retry_count: retryCount - 1, max_retries: maxRetries } }),
4625
+ id
4626
+ ]);
4627
+ } else {
4628
+ const backoffMinutes = Math.pow(5, retryCount - 1);
4629
+ const retryAfter = options.retry_after || new Date(Date.now() + backoffMinutes * 60 * 1000).toISOString();
4630
+ let title = task.title;
4631
+ if (task.short_id && title.startsWith(task.short_id + ": ")) {
4632
+ title = title.slice(task.short_id.length + 2);
4633
+ }
4634
+ retryTask = createTask({
4635
+ title,
4636
+ description: task.description ?? undefined,
4637
+ priority: task.priority,
4638
+ project_id: task.project_id ?? undefined,
4639
+ task_list_id: task.task_list_id ?? undefined,
4640
+ plan_id: task.plan_id ?? undefined,
4641
+ assigned_to: task.assigned_to ?? undefined,
4642
+ tags: task.tags,
4643
+ metadata: { ...task.metadata, _retry: { original_id: task.id, retry_count: retryCount, max_retries: maxRetries, retry_after: retryAfter, failure_reason: reason } },
4644
+ estimated_minutes: task.estimated_minutes ?? undefined,
4645
+ recurrence_rule: task.recurrence_rule ?? undefined,
4646
+ due_at: retryAfter
4647
+ }, d);
4648
+ d.run("UPDATE tasks SET retry_count = ?, max_retries = ?, retry_after = ? WHERE id = ?", [retryCount, maxRetries, retryAfter, retryTask.id]);
4566
4649
  }
4567
- retryTask = createTask({
4568
- title,
4569
- description: task.description ?? undefined,
4570
- priority: task.priority,
4571
- project_id: task.project_id ?? undefined,
4572
- task_list_id: task.task_list_id ?? undefined,
4573
- plan_id: task.plan_id ?? undefined,
4574
- assigned_to: task.assigned_to ?? undefined,
4575
- tags: task.tags,
4576
- metadata: { ...task.metadata, _retry: { original_id: task.id, retry_after: options.retry_after || null, failure_reason: reason } },
4577
- estimated_minutes: task.estimated_minutes ?? undefined,
4578
- recurrence_rule: task.recurrence_rule ?? undefined,
4579
- due_at: options.retry_after || task.due_at || undefined
4580
- }, d);
4581
4650
  }
4582
4651
  return { task: failedTask, retryTask };
4583
4652
  }
@@ -4601,6 +4670,40 @@ function getStaleTasks(staleMinutes = 30, filters, db) {
4601
4670
  const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at ASC`).all(...params);
4602
4671
  return rows.map(rowToTask);
4603
4672
  }
4673
+ function logCost(taskId, tokens, usd, db) {
4674
+ const d = db || getDatabase();
4675
+ d.run("UPDATE tasks SET cost_tokens = cost_tokens + ?, cost_usd = cost_usd + ?, updated_at = ? WHERE id = ?", [tokens, usd, now(), taskId]);
4676
+ }
4677
+ function stealTask(agentId, opts, db) {
4678
+ const d = db || getDatabase();
4679
+ const staleMinutes = opts?.stale_minutes ?? 30;
4680
+ const staleTasks = getStaleTasks(staleMinutes, { project_id: opts?.project_id, task_list_id: opts?.task_list_id }, d);
4681
+ if (staleTasks.length === 0)
4682
+ return null;
4683
+ const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
4684
+ staleTasks.sort((a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9));
4685
+ const target = staleTasks[0];
4686
+ const timestamp = now();
4687
+ d.run(`UPDATE tasks SET assigned_to = ?, locked_by = ?, locked_at = ?, updated_at = ?, version = version + 1 WHERE id = ?`, [agentId, agentId, timestamp, timestamp, target.id]);
4688
+ logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
4689
+ dispatchWebhook("task.assigned", { id: target.id, agent_id: agentId, title: target.title, stolen_from: target.assigned_to }, d).catch(() => {});
4690
+ return { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
4691
+ }
4692
+ function claimOrSteal(agentId, filters, db) {
4693
+ const d = db || getDatabase();
4694
+ const tx = d.transaction(() => {
4695
+ const next = getNextTask(agentId, filters, d);
4696
+ if (next) {
4697
+ const started = startTask(next.id, agentId, d);
4698
+ return { task: started, stolen: false };
4699
+ }
4700
+ const stolen = stealTask(agentId, { stale_minutes: filters?.stale_minutes, project_id: filters?.project_id, task_list_id: filters?.task_list_id }, d);
4701
+ if (stolen)
4702
+ return { task: stolen, stolen: true };
4703
+ return null;
4704
+ });
4705
+ return tx();
4706
+ }
4604
4707
  function getStatus(filters, agentId, options, db) {
4605
4708
  const d = db || getDatabase();
4606
4709
  const pending = countTasks({ ...filters, status: "pending" }, d);
@@ -10363,6 +10466,219 @@ var init_zod = __esm(() => {
10363
10466
  init_external();
10364
10467
  });
10365
10468
 
10469
+ // src/db/traces.ts
10470
+ var exports_traces = {};
10471
+ __export(exports_traces, {
10472
+ logTrace: () => logTrace,
10473
+ getTraceStats: () => getTraceStats,
10474
+ getTaskTraces: () => getTaskTraces
10475
+ });
10476
+ function logTrace(input, db) {
10477
+ const d = db || getDatabase();
10478
+ const id = uuid();
10479
+ const timestamp = now();
10480
+ 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)
10481
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
10482
+ id,
10483
+ input.task_id,
10484
+ input.agent_id || null,
10485
+ input.trace_type,
10486
+ input.name || null,
10487
+ input.input_summary || null,
10488
+ input.output_summary || null,
10489
+ input.duration_ms ?? null,
10490
+ input.tokens ?? null,
10491
+ input.cost_usd ?? null,
10492
+ timestamp
10493
+ ]);
10494
+ return {
10495
+ id,
10496
+ task_id: input.task_id,
10497
+ agent_id: input.agent_id || null,
10498
+ trace_type: input.trace_type,
10499
+ name: input.name || null,
10500
+ input_summary: input.input_summary || null,
10501
+ output_summary: input.output_summary || null,
10502
+ duration_ms: input.duration_ms ?? null,
10503
+ tokens: input.tokens ?? null,
10504
+ cost_usd: input.cost_usd ?? null,
10505
+ created_at: timestamp
10506
+ };
10507
+ }
10508
+ function getTaskTraces(taskId, db) {
10509
+ const d = db || getDatabase();
10510
+ return d.query("SELECT * FROM task_traces WHERE task_id = ? ORDER BY created_at DESC").all(taskId);
10511
+ }
10512
+ function getTraceStats(taskId, db) {
10513
+ const d = db || getDatabase();
10514
+ const row = d.query(`SELECT COUNT(*) as total,
10515
+ SUM(CASE WHEN trace_type = 'tool_call' THEN 1 ELSE 0 END) as tool_calls,
10516
+ SUM(CASE WHEN trace_type = 'llm_call' THEN 1 ELSE 0 END) as llm_calls,
10517
+ SUM(CASE WHEN trace_type = 'error' THEN 1 ELSE 0 END) as errors,
10518
+ COALESCE(SUM(tokens), 0) as total_tokens,
10519
+ COALESCE(SUM(cost_usd), 0) as total_cost_usd,
10520
+ COALESCE(SUM(duration_ms), 0) as total_duration_ms
10521
+ FROM task_traces WHERE task_id = ?`).get(taskId);
10522
+ return row;
10523
+ }
10524
+ var init_traces = __esm(() => {
10525
+ init_database();
10526
+ });
10527
+
10528
+ // src/db/snapshots.ts
10529
+ var exports_snapshots = {};
10530
+ __export(exports_snapshots, {
10531
+ saveSnapshot: () => saveSnapshot,
10532
+ listSnapshots: () => listSnapshots,
10533
+ getLatestSnapshot: () => getLatestSnapshot
10534
+ });
10535
+ function rowToSnapshot(row) {
10536
+ return {
10537
+ ...row,
10538
+ snapshot_type: row.snapshot_type,
10539
+ files_open: JSON.parse(row.files_open || "[]"),
10540
+ attempts: JSON.parse(row.attempts || "[]"),
10541
+ blockers: JSON.parse(row.blockers || "[]"),
10542
+ metadata: JSON.parse(row.metadata || "{}")
10543
+ };
10544
+ }
10545
+ function saveSnapshot(input, db) {
10546
+ const d = db || getDatabase();
10547
+ const id = uuid();
10548
+ const timestamp = now();
10549
+ 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)
10550
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
10551
+ id,
10552
+ input.agent_id || null,
10553
+ input.task_id || null,
10554
+ input.project_id || null,
10555
+ input.snapshot_type,
10556
+ input.plan_summary || null,
10557
+ JSON.stringify(input.files_open || []),
10558
+ JSON.stringify(input.attempts || []),
10559
+ JSON.stringify(input.blockers || []),
10560
+ input.next_steps || null,
10561
+ JSON.stringify(input.metadata || {}),
10562
+ timestamp
10563
+ ]);
10564
+ return {
10565
+ id,
10566
+ agent_id: input.agent_id || null,
10567
+ task_id: input.task_id || null,
10568
+ project_id: input.project_id || null,
10569
+ snapshot_type: input.snapshot_type,
10570
+ plan_summary: input.plan_summary || null,
10571
+ files_open: input.files_open || [],
10572
+ attempts: input.attempts || [],
10573
+ blockers: input.blockers || [],
10574
+ next_steps: input.next_steps || null,
10575
+ metadata: input.metadata || {},
10576
+ created_at: timestamp
10577
+ };
10578
+ }
10579
+ function getLatestSnapshot(agentId, taskId, db) {
10580
+ const d = db || getDatabase();
10581
+ const conditions = [];
10582
+ const params = [];
10583
+ if (agentId) {
10584
+ conditions.push("agent_id = ?");
10585
+ params.push(agentId);
10586
+ }
10587
+ if (taskId) {
10588
+ conditions.push("task_id = ?");
10589
+ params.push(taskId);
10590
+ }
10591
+ if (conditions.length === 0)
10592
+ return null;
10593
+ const where = conditions.join(" AND ");
10594
+ const row = d.query(`SELECT * FROM context_snapshots WHERE ${where} ORDER BY created_at DESC LIMIT 1`).get(...params);
10595
+ return row ? rowToSnapshot(row) : null;
10596
+ }
10597
+ function listSnapshots(opts, db) {
10598
+ const d = db || getDatabase();
10599
+ const conditions = [];
10600
+ const params = [];
10601
+ if (opts.agent_id) {
10602
+ conditions.push("agent_id = ?");
10603
+ params.push(opts.agent_id);
10604
+ }
10605
+ if (opts.task_id) {
10606
+ conditions.push("task_id = ?");
10607
+ params.push(opts.task_id);
10608
+ }
10609
+ if (opts.project_id) {
10610
+ conditions.push("project_id = ?");
10611
+ params.push(opts.project_id);
10612
+ }
10613
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
10614
+ const limit = opts.limit || 20;
10615
+ params.push(limit);
10616
+ return d.query(`SELECT * FROM context_snapshots ${where} ORDER BY created_at DESC LIMIT ?`).all(...params).map(rowToSnapshot);
10617
+ }
10618
+ var init_snapshots = __esm(() => {
10619
+ init_database();
10620
+ });
10621
+
10622
+ // src/db/budgets.ts
10623
+ var exports_budgets = {};
10624
+ __export(exports_budgets, {
10625
+ setBudget: () => setBudget,
10626
+ getBudget: () => getBudget,
10627
+ checkBudget: () => checkBudget
10628
+ });
10629
+ function setBudget(agentId, opts, db) {
10630
+ const d = db || getDatabase();
10631
+ const timestamp = now();
10632
+ d.run(`INSERT INTO agent_budgets (agent_id, max_concurrent, max_cost_usd, max_task_minutes, period_hours, created_at, updated_at)
10633
+ VALUES (?, ?, ?, ?, ?, ?, ?)
10634
+ ON CONFLICT(agent_id) DO UPDATE SET
10635
+ max_concurrent = COALESCE(?, max_concurrent),
10636
+ max_cost_usd = COALESCE(?, max_cost_usd),
10637
+ max_task_minutes = COALESCE(?, max_task_minutes),
10638
+ period_hours = COALESCE(?, period_hours),
10639
+ updated_at = ?`, [
10640
+ agentId,
10641
+ opts.max_concurrent ?? 5,
10642
+ opts.max_cost_usd ?? null,
10643
+ opts.max_task_minutes ?? null,
10644
+ opts.period_hours ?? 24,
10645
+ timestamp,
10646
+ timestamp,
10647
+ opts.max_concurrent ?? null,
10648
+ opts.max_cost_usd ?? null,
10649
+ opts.max_task_minutes ?? null,
10650
+ opts.period_hours ?? null,
10651
+ timestamp
10652
+ ]);
10653
+ return getBudget(agentId, d);
10654
+ }
10655
+ function getBudget(agentId, db) {
10656
+ const d = db || getDatabase();
10657
+ return d.query("SELECT * FROM agent_budgets WHERE agent_id = ?").get(agentId);
10658
+ }
10659
+ function checkBudget(agentId, db) {
10660
+ const d = db || getDatabase();
10661
+ const budget = getBudget(agentId, d);
10662
+ if (!budget)
10663
+ return { allowed: true, current_concurrent: 0, max_concurrent: 999 };
10664
+ const concurrent = countTasks({ status: "in_progress", assigned_to: agentId }, d);
10665
+ if (concurrent >= budget.max_concurrent) {
10666
+ return { allowed: false, reason: `Concurrent limit reached (${concurrent}/${budget.max_concurrent})`, current_concurrent: concurrent, max_concurrent: budget.max_concurrent };
10667
+ }
10668
+ if (budget.max_cost_usd != null) {
10669
+ const periodStart = new Date(Date.now() - budget.period_hours * 60 * 60 * 1000).toISOString();
10670
+ 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);
10671
+ if (costRow.total >= budget.max_cost_usd) {
10672
+ 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 };
10673
+ }
10674
+ }
10675
+ return { allowed: true, current_concurrent: concurrent, max_concurrent: budget.max_concurrent };
10676
+ }
10677
+ var init_budgets = __esm(() => {
10678
+ init_database();
10679
+ init_tasks();
10680
+ });
10681
+
10366
10682
  // src/lib/github.ts
10367
10683
  var exports_github = {};
10368
10684
  __export(exports_github, {
@@ -12652,6 +12968,33 @@ ID: ${updated.id}`
12652
12968
  }
12653
12969
  });
12654
12970
  }
12971
+ if (shouldRegisterTool("update_agent")) {
12972
+ server.tool("update_agent", "Update an agent's description, role, title, or other metadata. Resolve by id or name.", {
12973
+ id: exports_external.string().optional(),
12974
+ name: exports_external.string().optional(),
12975
+ description: exports_external.string().optional(),
12976
+ role: exports_external.string().optional(),
12977
+ title: exports_external.string().optional(),
12978
+ level: exports_external.string().optional(),
12979
+ capabilities: exports_external.array(exports_external.string()).optional(),
12980
+ permissions: exports_external.array(exports_external.string()).optional(),
12981
+ metadata: exports_external.record(exports_external.unknown()).optional()
12982
+ }, async ({ id, name, ...updates }) => {
12983
+ try {
12984
+ if (!id && !name) {
12985
+ return { content: [{ type: "text", text: "Provide either id or name." }], isError: true };
12986
+ }
12987
+ const agent = id ? getAgent(id) : getAgentByName(name);
12988
+ if (!agent) {
12989
+ return { content: [{ type: "text", text: `Agent not found: ${id || name}` }], isError: true };
12990
+ }
12991
+ const updated = updateAgent(agent.id, updates);
12992
+ return { content: [{ type: "text", text: `Agent updated: ${updated.name} (${updated.id.slice(0, 8)})` }] };
12993
+ } catch (e) {
12994
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
12995
+ }
12996
+ });
12997
+ }
12655
12998
  if (shouldRegisterTool("delete_agent")) {
12656
12999
  server.tool("delete_agent", "Archive an agent (soft delete). The agent is hidden from list_agents but preserved for task history. Use unarchive_agent to restore. Resolve by id or name.", {
12657
13000
  id: exports_external.string().optional(),
@@ -13010,6 +13353,135 @@ Blocked:`);
13010
13353
  }
13011
13354
  });
13012
13355
  }
13356
+ if (shouldRegisterTool("log_cost")) {
13357
+ 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 }) => {
13358
+ try {
13359
+ const { logCost: logCost2 } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
13360
+ const resolvedId = resolveId(task_id, "tasks");
13361
+ logCost2(resolvedId, tokens, usd);
13362
+ return { content: [{ type: "text", text: `Logged ${tokens} tokens ($${usd.toFixed(4)}) to ${task_id}` }] };
13363
+ } catch (e) {
13364
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
13365
+ }
13366
+ });
13367
+ }
13368
+ if (shouldRegisterTool("log_trace")) {
13369
+ server.tool("log_trace", "Log a trace entry (tool call, LLM call, error, handoff) to a task for observability.", {
13370
+ task_id: exports_external.string(),
13371
+ agent_id: exports_external.string().optional(),
13372
+ trace_type: exports_external.enum(["tool_call", "llm_call", "error", "handoff", "custom"]),
13373
+ name: exports_external.string().optional(),
13374
+ input_summary: exports_external.string().optional(),
13375
+ output_summary: exports_external.string().optional(),
13376
+ duration_ms: exports_external.number().optional(),
13377
+ tokens: exports_external.number().optional(),
13378
+ cost_usd: exports_external.number().optional()
13379
+ }, async ({ task_id, agent_id, trace_type, name, input_summary, output_summary, duration_ms, tokens, cost_usd }) => {
13380
+ try {
13381
+ const { logTrace: logTrace2 } = await Promise.resolve().then(() => (init_traces(), exports_traces));
13382
+ const resolvedId = resolveId(task_id, "tasks");
13383
+ const trace = logTrace2({ task_id: resolvedId, agent_id, trace_type, name, input_summary, output_summary, duration_ms, tokens, cost_usd });
13384
+ return { content: [{ type: "text", text: `Trace logged: ${trace.id} [${trace_type}] ${name || ""}` }] };
13385
+ } catch (e) {
13386
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
13387
+ }
13388
+ });
13389
+ }
13390
+ if (shouldRegisterTool("get_traces")) {
13391
+ 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 }) => {
13392
+ try {
13393
+ const { getTaskTraces: getTaskTraces2, getTraceStats: getTraceStats2 } = await Promise.resolve().then(() => (init_traces(), exports_traces));
13394
+ const resolvedId = resolveId(task_id, "tasks");
13395
+ const traces = getTaskTraces2(resolvedId);
13396
+ const stats = getTraceStats2(resolvedId);
13397
+ 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`];
13398
+ for (const t of traces.slice(0, 20)) {
13399
+ lines.push(` ${t.created_at} [${t.trace_type}] ${t.name || ""} ${t.tokens ? t.tokens + "tok" : ""} ${t.duration_ms ? t.duration_ms + "ms" : ""}`);
13400
+ }
13401
+ return { content: [{ type: "text", text: lines.join(`
13402
+ `) }] };
13403
+ } catch (e) {
13404
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
13405
+ }
13406
+ });
13407
+ }
13408
+ if (shouldRegisterTool("save_snapshot")) {
13409
+ 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.", {
13410
+ agent_id: exports_external.string().optional(),
13411
+ task_id: exports_external.string().optional(),
13412
+ project_id: exports_external.string().optional(),
13413
+ snapshot_type: exports_external.enum(["interrupt", "complete", "handoff", "checkpoint"]),
13414
+ plan_summary: exports_external.string().optional(),
13415
+ files_open: exports_external.array(exports_external.string()).optional(),
13416
+ attempts: exports_external.array(exports_external.string()).optional(),
13417
+ blockers: exports_external.array(exports_external.string()).optional(),
13418
+ next_steps: exports_external.string().optional()
13419
+ }, async ({ agent_id, task_id, project_id, snapshot_type, plan_summary, files_open, attempts, blockers, next_steps }) => {
13420
+ try {
13421
+ const { saveSnapshot: saveSnapshot2 } = await Promise.resolve().then(() => (init_snapshots(), exports_snapshots));
13422
+ const snap = saveSnapshot2({ agent_id, task_id, project_id, snapshot_type, plan_summary, files_open, attempts, blockers, next_steps });
13423
+ return { content: [{ type: "text", text: `Snapshot saved: ${snap.id} [${snapshot_type}]${plan_summary ? " \u2014 " + plan_summary.slice(0, 80) : ""}` }] };
13424
+ } catch (e) {
13425
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
13426
+ }
13427
+ });
13428
+ }
13429
+ if (shouldRegisterTool("get_snapshot")) {
13430
+ 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 }) => {
13431
+ try {
13432
+ const { getLatestSnapshot: getLatestSnapshot2 } = await Promise.resolve().then(() => (init_snapshots(), exports_snapshots));
13433
+ const snap = getLatestSnapshot2(agent_id, task_id);
13434
+ if (!snap)
13435
+ return { content: [{ type: "text", text: "No snapshot found." }] };
13436
+ const lines = [`Snapshot [${snap.snapshot_type}] from ${snap.created_at}`];
13437
+ if (snap.plan_summary)
13438
+ lines.push(`Plan: ${snap.plan_summary}`);
13439
+ if (snap.files_open.length > 0)
13440
+ lines.push(`Files: ${snap.files_open.join(", ")}`);
13441
+ if (snap.blockers.length > 0)
13442
+ lines.push(`Blockers: ${snap.blockers.join(", ")}`);
13443
+ if (snap.next_steps)
13444
+ lines.push(`Next: ${snap.next_steps}`);
13445
+ if (snap.attempts.length > 0)
13446
+ lines.push(`Attempts: ${snap.attempts.join("; ")}`);
13447
+ return { content: [{ type: "text", text: lines.join(`
13448
+ `) }] };
13449
+ } catch (e) {
13450
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
13451
+ }
13452
+ });
13453
+ }
13454
+ if (shouldRegisterTool("set_budget")) {
13455
+ server.tool("set_budget", "Set execution budget for an agent \u2014 max concurrent tasks, cost limit, time limit per period.", {
13456
+ agent_id: exports_external.string(),
13457
+ max_concurrent: exports_external.number().optional(),
13458
+ max_cost_usd: exports_external.number().optional(),
13459
+ max_task_minutes: exports_external.number().optional(),
13460
+ period_hours: exports_external.number().optional()
13461
+ }, async ({ agent_id, max_concurrent, max_cost_usd, max_task_minutes, period_hours }) => {
13462
+ try {
13463
+ const { setBudget: setBudget2 } = await Promise.resolve().then(() => (init_budgets(), exports_budgets));
13464
+ const budget = setBudget2(agent_id, { max_concurrent, max_cost_usd, max_task_minutes, period_hours });
13465
+ 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` }] };
13466
+ } catch (e) {
13467
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
13468
+ }
13469
+ });
13470
+ }
13471
+ if (shouldRegisterTool("check_budget")) {
13472
+ 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 }) => {
13473
+ try {
13474
+ const { checkBudget: checkBudget2 } = await Promise.resolve().then(() => (init_budgets(), exports_budgets));
13475
+ const result = checkBudget2(agent_id);
13476
+ if (result.allowed) {
13477
+ return { content: [{ type: "text", text: `Budget OK: ${result.current_concurrent}/${result.max_concurrent} concurrent tasks` }] };
13478
+ }
13479
+ return { content: [{ type: "text", text: `BUDGET EXCEEDED: ${result.reason}` }], isError: true };
13480
+ } catch (e) {
13481
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
13482
+ }
13483
+ });
13484
+ }
13013
13485
  if (shouldRegisterTool("import_github_issue")) {
13014
13486
  server.tool("import_github_issue", "Import a GitHub issue as a task. Requires gh CLI installed and authenticated.", {
13015
13487
  url: exports_external.string().describe("GitHub issue URL (e.g. https://github.com/owner/repo/issues/42)"),
@@ -13720,6 +14192,56 @@ ${formatTaskDetail(task, 300)}` }] };
13720
14192
  }
13721
14193
  });
13722
14194
  }
14195
+ if (shouldRegisterTool("steal_task")) {
14196
+ server.tool("steal_task", "Work-stealing: take the highest-priority stale in_progress task from another agent and reassign it to you.", {
14197
+ agent_id: exports_external.string().describe("Your agent ID"),
14198
+ stale_minutes: exports_external.number().optional().describe("How long a task must be stale before stealing (default: 30)"),
14199
+ project_id: exports_external.string().optional(),
14200
+ task_list_id: exports_external.string().optional()
14201
+ }, async ({ agent_id, stale_minutes, project_id, task_list_id }) => {
14202
+ try {
14203
+ const task = stealTask(agent_id, { stale_minutes, project_id, task_list_id });
14204
+ if (!task)
14205
+ return { content: [{ type: "text", text: "No stale tasks available to steal." }] };
14206
+ return { content: [{ type: "text", text: `Stolen: ${formatTask(task)}
14207
+ Previous owner: ${task.metadata?._stolen_from || "unknown"}` }] };
14208
+ } catch (e) {
14209
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
14210
+ }
14211
+ });
14212
+ }
14213
+ if (shouldRegisterTool("claim_or_steal")) {
14214
+ 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.", {
14215
+ agent_id: exports_external.string(),
14216
+ project_id: exports_external.string().optional(),
14217
+ task_list_id: exports_external.string().optional(),
14218
+ plan_id: exports_external.string().optional(),
14219
+ tags: exports_external.array(exports_external.string()).optional(),
14220
+ stale_minutes: exports_external.number().optional().describe("Stale threshold for work-stealing fallback (default: 30)")
14221
+ }, async ({ agent_id, project_id, task_list_id, plan_id, tags, stale_minutes }) => {
14222
+ try {
14223
+ const filters = {};
14224
+ if (project_id)
14225
+ filters.project_id = resolveId(project_id, "projects");
14226
+ if (task_list_id)
14227
+ filters.task_list_id = resolveId(task_list_id, "task_lists");
14228
+ if (plan_id)
14229
+ filters.plan_id = resolveId(plan_id, "plans");
14230
+ if (tags)
14231
+ filters.tags = tags;
14232
+ if (stale_minutes)
14233
+ filters.stale_minutes = stale_minutes;
14234
+ const result = claimOrSteal(agent_id, Object.keys(filters).length > 0 ? filters : undefined);
14235
+ if (!result)
14236
+ return { content: [{ type: "text", text: "No tasks available to claim or steal." }] };
14237
+ const prefix = result.stolen ? "Stolen" : "Claimed";
14238
+ return { content: [{ type: "text", text: `${prefix}: ${formatTask(result.task)}
14239
+ ${formatTaskDetail(result.task, 300)}` }] };
14240
+ } catch (e) {
14241
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
14242
+ }
14243
+ });
14244
+ }
13723
14245
  if (shouldRegisterTool("get_stale_tasks")) {
13724
14246
  server.tool("get_stale_tasks", "Find stale in_progress tasks with no recent activity.", {
13725
14247
  stale_minutes: exports_external.number().optional(),
@@ -19236,6 +19758,36 @@ program2.command("agents").description("List registered agents").action(() => {
19236
19758
  handleError(e);
19237
19759
  }
19238
19760
  });
19761
+ program2.command("agent-update <name>").alias("agents-update").description("Update an agent's description, role, or other fields").option("--description <text>", "New description").option("--role <role>", "New role").option("--title <title>", "New title").action((name, opts) => {
19762
+ const globalOpts = program2.opts();
19763
+ try {
19764
+ const { getAgentByName: findByName, updateAgent: doUpdate } = (init_agents(), __toCommonJS(exports_agents));
19765
+ const agent = findByName(name);
19766
+ if (!agent) {
19767
+ console.error(chalk.red(`Agent not found: ${name}`));
19768
+ process.exit(1);
19769
+ }
19770
+ const updates = {};
19771
+ if (opts.description !== undefined)
19772
+ updates.description = opts.description;
19773
+ if (opts.role !== undefined)
19774
+ updates.role = opts.role;
19775
+ if (opts.title !== undefined)
19776
+ updates.title = opts.title;
19777
+ const updated = doUpdate(agent.id, updates);
19778
+ if (globalOpts.json) {
19779
+ output(updated, true);
19780
+ } else {
19781
+ console.log(chalk.green(`Updated agent: ${updated.name} (${updated.id.slice(0, 8)})`));
19782
+ if (updated.description)
19783
+ console.log(chalk.dim(` Description: ${updated.description}`));
19784
+ if (updated.role)
19785
+ console.log(chalk.dim(` Role: ${updated.role}`));
19786
+ }
19787
+ } catch (e) {
19788
+ handleError(e);
19789
+ }
19790
+ });
19239
19791
  program2.command("agent <name>").description("Show all info about an agent: tasks, status, last seen, stats").option("--json", "Output as JSON").action((name, opts) => {
19240
19792
  const globalOpts = program2.opts();
19241
19793
  const { getAgentByName: findByName } = (init_agents(), __toCommonJS(exports_agents));
@@ -19701,6 +20253,20 @@ program2.command("claim <agent>").description("Atomically claim the best pending
19701
20253
  }
19702
20254
  console.log(chalk.green(`Claimed: ${task.short_id || task.id.slice(0, 8)} | ${task.priority} | ${task.title}`));
19703
20255
  });
20256
+ program2.command("steal <agent>").description("Work-stealing: take the highest-priority stale task from another agent").option("--stale-minutes <n>", "How long a task must be stale (default: 30)", "30").option("--project <id>", "Filter to project").action((agent, opts) => {
20257
+ const globalOpts = program2.opts();
20258
+ const { stealTask: stealTask2 } = (init_tasks(), __toCommonJS(exports_tasks));
20259
+ const task = stealTask2(agent, { stale_minutes: parseInt(opts.staleMinutes, 10), project_id: opts.project });
20260
+ if (!task) {
20261
+ console.log(chalk.dim("No stale tasks available to steal."));
20262
+ return;
20263
+ }
20264
+ if (globalOpts.json) {
20265
+ output(task, true);
20266
+ return;
20267
+ }
20268
+ console.log(chalk.green(`Stolen: ${task.short_id || task.id.slice(0, 8)} | ${task.priority} | ${task.title}`));
20269
+ });
19704
20270
  program2.command("status").description("Show full project health snapshot").option("--agent <id>", "Include next task for this agent").option("--project <id>", "Filter to project").option("--json", "Output as JSON").action(async (opts) => {
19705
20271
  const db = getDatabase();
19706
20272
  const filters = {};