@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/index.js CHANGED
@@ -754,6 +754,58 @@ var MIGRATIONS = [
754
754
  `
755
755
  ALTER TABLE tasks ADD COLUMN task_type TEXT;
756
756
  CREATE INDEX IF NOT EXISTS idx_tasks_task_type ON tasks(task_type);
757
+ ALTER TABLE tasks ADD COLUMN cost_tokens INTEGER DEFAULT 0;
758
+ ALTER TABLE tasks ADD COLUMN cost_usd REAL DEFAULT 0;
759
+ ALTER TABLE tasks ADD COLUMN delegated_from TEXT;
760
+ ALTER TABLE tasks ADD COLUMN delegation_depth INTEGER DEFAULT 0;
761
+ ALTER TABLE tasks ADD COLUMN retry_count INTEGER DEFAULT 0;
762
+ ALTER TABLE tasks ADD COLUMN max_retries INTEGER DEFAULT 3;
763
+ ALTER TABLE tasks ADD COLUMN retry_after TEXT;
764
+ ALTER TABLE tasks ADD COLUMN sla_minutes INTEGER;
765
+
766
+ CREATE TABLE IF NOT EXISTS task_traces (
767
+ id TEXT PRIMARY KEY,
768
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
769
+ agent_id TEXT,
770
+ trace_type TEXT NOT NULL CHECK(trace_type IN ('tool_call','llm_call','error','handoff','custom')),
771
+ name TEXT,
772
+ input_summary TEXT,
773
+ output_summary TEXT,
774
+ duration_ms INTEGER,
775
+ tokens INTEGER,
776
+ cost_usd REAL,
777
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
778
+ );
779
+ CREATE INDEX IF NOT EXISTS idx_task_traces_task ON task_traces(task_id);
780
+ CREATE INDEX IF NOT EXISTS idx_task_traces_agent ON task_traces(agent_id);
781
+
782
+ CREATE TABLE IF NOT EXISTS context_snapshots (
783
+ id TEXT PRIMARY KEY,
784
+ agent_id TEXT,
785
+ task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
786
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
787
+ snapshot_type TEXT NOT NULL CHECK(snapshot_type IN ('interrupt','complete','handoff','checkpoint')),
788
+ plan_summary TEXT,
789
+ files_open TEXT DEFAULT '[]',
790
+ attempts TEXT DEFAULT '[]',
791
+ blockers TEXT DEFAULT '[]',
792
+ next_steps TEXT,
793
+ metadata TEXT DEFAULT '{}',
794
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
795
+ );
796
+ CREATE INDEX IF NOT EXISTS idx_snapshots_agent ON context_snapshots(agent_id);
797
+ CREATE INDEX IF NOT EXISTS idx_snapshots_task ON context_snapshots(task_id);
798
+
799
+ CREATE TABLE IF NOT EXISTS agent_budgets (
800
+ agent_id TEXT PRIMARY KEY,
801
+ max_concurrent INTEGER DEFAULT 5,
802
+ max_cost_usd REAL,
803
+ max_task_minutes INTEGER,
804
+ period_hours INTEGER DEFAULT 24,
805
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
806
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
807
+ );
808
+
757
809
  INSERT OR IGNORE INTO _migrations (id) VALUES (35);
758
810
  `
759
811
  ];
@@ -936,6 +988,15 @@ function ensureSchema(db) {
936
988
  ensureColumn("tasks", "assigned_by", "TEXT");
937
989
  ensureColumn("tasks", "assigned_from_project", "TEXT");
938
990
  ensureColumn("tasks", "started_at", "TEXT");
991
+ ensureColumn("tasks", "task_type", "TEXT");
992
+ ensureColumn("tasks", "cost_tokens", "INTEGER DEFAULT 0");
993
+ ensureColumn("tasks", "cost_usd", "REAL DEFAULT 0");
994
+ ensureColumn("tasks", "delegated_from", "TEXT");
995
+ ensureColumn("tasks", "delegation_depth", "INTEGER DEFAULT 0");
996
+ ensureColumn("tasks", "retry_count", "INTEGER DEFAULT 0");
997
+ ensureColumn("tasks", "max_retries", "INTEGER DEFAULT 3");
998
+ ensureColumn("tasks", "retry_after", "TEXT");
999
+ ensureColumn("tasks", "sla_minutes", "INTEGER");
939
1000
  ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
940
1001
  ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
941
1002
  ensureColumn("agents", "reports_to", "TEXT");
@@ -2252,6 +2313,20 @@ function completeTask(id, agentId, db, options) {
2252
2313
  if (spawnedFromTemplate) {
2253
2314
  meta._spawned_task = { id: spawnedFromTemplate.id, short_id: spawnedFromTemplate.short_id, title: spawnedFromTemplate.title };
2254
2315
  }
2316
+ const unblockedDeps = d.query(`SELECT DISTINCT t.id, t.short_id, t.title FROM tasks t
2317
+ JOIN task_dependencies td ON td.task_id = t.id
2318
+ WHERE td.depends_on = ? AND t.status = 'pending'
2319
+ AND NOT EXISTS (
2320
+ SELECT 1 FROM task_dependencies td2
2321
+ JOIN tasks dep2 ON dep2.id = td2.depends_on
2322
+ WHERE td2.task_id = t.id AND dep2.status NOT IN ('completed', 'cancelled') AND dep2.id != ?
2323
+ )`).all(id, id);
2324
+ if (unblockedDeps.length > 0) {
2325
+ meta._unblocked = unblockedDeps.map((d2) => ({ id: d2.id, short_id: d2.short_id, title: d2.title }));
2326
+ for (const dep of unblockedDeps) {
2327
+ dispatchWebhook("task.unblocked", { id: dep.id, unblocked_by: id, title: dep.title }, d).catch(() => {});
2328
+ }
2329
+ }
2255
2330
  return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: task.version + 1, updated_at: timestamp, metadata: meta };
2256
2331
  }
2257
2332
  function lockTask(id, agentId, db) {
@@ -2548,24 +2623,36 @@ function failTask(id, agentId, reason, options, db) {
2548
2623
  };
2549
2624
  let retryTask;
2550
2625
  if (options?.retry) {
2551
- let title = task.title;
2552
- if (task.short_id && title.startsWith(task.short_id + ": ")) {
2553
- title = title.slice(task.short_id.length + 2);
2626
+ const retryCount = (task.retry_count || 0) + 1;
2627
+ const maxRetries = task.max_retries || 3;
2628
+ if (retryCount > maxRetries) {
2629
+ d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [
2630
+ JSON.stringify({ ...meta, _retry_exhausted: { retry_count: retryCount - 1, max_retries: maxRetries } }),
2631
+ id
2632
+ ]);
2633
+ } else {
2634
+ const backoffMinutes = Math.pow(5, retryCount - 1);
2635
+ const retryAfter = options.retry_after || new Date(Date.now() + backoffMinutes * 60 * 1000).toISOString();
2636
+ let title = task.title;
2637
+ if (task.short_id && title.startsWith(task.short_id + ": ")) {
2638
+ title = title.slice(task.short_id.length + 2);
2639
+ }
2640
+ retryTask = createTask({
2641
+ title,
2642
+ description: task.description ?? undefined,
2643
+ priority: task.priority,
2644
+ project_id: task.project_id ?? undefined,
2645
+ task_list_id: task.task_list_id ?? undefined,
2646
+ plan_id: task.plan_id ?? undefined,
2647
+ assigned_to: task.assigned_to ?? undefined,
2648
+ tags: task.tags,
2649
+ metadata: { ...task.metadata, _retry: { original_id: task.id, retry_count: retryCount, max_retries: maxRetries, retry_after: retryAfter, failure_reason: reason } },
2650
+ estimated_minutes: task.estimated_minutes ?? undefined,
2651
+ recurrence_rule: task.recurrence_rule ?? undefined,
2652
+ due_at: retryAfter
2653
+ }, d);
2654
+ d.run("UPDATE tasks SET retry_count = ?, max_retries = ?, retry_after = ? WHERE id = ?", [retryCount, maxRetries, retryAfter, retryTask.id]);
2554
2655
  }
2555
- retryTask = createTask({
2556
- title,
2557
- description: task.description ?? undefined,
2558
- priority: task.priority,
2559
- project_id: task.project_id ?? undefined,
2560
- task_list_id: task.task_list_id ?? undefined,
2561
- plan_id: task.plan_id ?? undefined,
2562
- assigned_to: task.assigned_to ?? undefined,
2563
- tags: task.tags,
2564
- metadata: { ...task.metadata, _retry: { original_id: task.id, retry_after: options.retry_after || null, failure_reason: reason } },
2565
- estimated_minutes: task.estimated_minutes ?? undefined,
2566
- recurrence_rule: task.recurrence_rule ?? undefined,
2567
- due_at: options.retry_after || task.due_at || undefined
2568
- }, d);
2569
2656
  }
2570
2657
  return { task: failedTask, retryTask };
2571
2658
  }
@@ -2589,6 +2676,40 @@ function getStaleTasks(staleMinutes = 30, filters, db) {
2589
2676
  const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at ASC`).all(...params);
2590
2677
  return rows.map(rowToTask);
2591
2678
  }
2679
+ function logCost(taskId, tokens, usd, db) {
2680
+ const d = db || getDatabase();
2681
+ d.run("UPDATE tasks SET cost_tokens = cost_tokens + ?, cost_usd = cost_usd + ?, updated_at = ? WHERE id = ?", [tokens, usd, now(), taskId]);
2682
+ }
2683
+ function stealTask(agentId, opts, db) {
2684
+ const d = db || getDatabase();
2685
+ const staleMinutes = opts?.stale_minutes ?? 30;
2686
+ const staleTasks = getStaleTasks(staleMinutes, { project_id: opts?.project_id, task_list_id: opts?.task_list_id }, d);
2687
+ if (staleTasks.length === 0)
2688
+ return null;
2689
+ const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
2690
+ staleTasks.sort((a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9));
2691
+ const target = staleTasks[0];
2692
+ const timestamp = now();
2693
+ d.run(`UPDATE tasks SET assigned_to = ?, locked_by = ?, locked_at = ?, updated_at = ?, version = version + 1 WHERE id = ?`, [agentId, agentId, timestamp, timestamp, target.id]);
2694
+ logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
2695
+ dispatchWebhook("task.assigned", { id: target.id, agent_id: agentId, title: target.title, stolen_from: target.assigned_to }, d).catch(() => {});
2696
+ return { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
2697
+ }
2698
+ function claimOrSteal(agentId, filters, db) {
2699
+ const d = db || getDatabase();
2700
+ const tx = d.transaction(() => {
2701
+ const next = getNextTask(agentId, filters, d);
2702
+ if (next) {
2703
+ const started = startTask(next.id, agentId, d);
2704
+ return { task: started, stolen: false };
2705
+ }
2706
+ const stolen = stealTask(agentId, { stale_minutes: filters?.stale_minutes, project_id: filters?.project_id, task_list_id: filters?.task_list_id }, d);
2707
+ if (stolen)
2708
+ return { task: stolen, stolen: true };
2709
+ return null;
2710
+ });
2711
+ return tx();
2712
+ }
2592
2713
  function getStatus(filters, agentId, options, db) {
2593
2714
  const d = db || getDatabase();
2594
2715
  const pending = countTasks({ ...filters, status: "pending" }, d);
@@ -3330,6 +3451,123 @@ function deleteSession(id, db) {
3330
3451
  const result = d.run("DELETE FROM sessions WHERE id = ?", [id]);
3331
3452
  return result.changes > 0;
3332
3453
  }
3454
+ // src/lib/gatherer.ts
3455
+ var SYSTEM_PROMPT = "You are a task management assistant that creates, updates, and tracks tasks and projects.";
3456
+ function taskToCreateExample(task) {
3457
+ const userMsg = `Create a task: ${task.title}${task.description ? `
3458
+
3459
+ Description: ${task.description}` : ""}`;
3460
+ const taskDetails = {
3461
+ id: task.short_id ?? task.id,
3462
+ title: task.title,
3463
+ description: task.description ?? "",
3464
+ status: task.status,
3465
+ priority: task.priority,
3466
+ tags: task.tags,
3467
+ created_at: task.created_at
3468
+ };
3469
+ return {
3470
+ messages: [
3471
+ { role: "system", content: SYSTEM_PROMPT },
3472
+ { role: "user", content: userMsg },
3473
+ {
3474
+ role: "assistant",
3475
+ content: `Created task: ${JSON.stringify(taskDetails, null, 2)}`
3476
+ }
3477
+ ]
3478
+ };
3479
+ }
3480
+ function taskToStatusUpdateExample(task) {
3481
+ if (!task.completed_at && task.status === "pending")
3482
+ return null;
3483
+ const id = task.short_id ?? task.id;
3484
+ return {
3485
+ messages: [
3486
+ { role: "system", content: SYSTEM_PROMPT },
3487
+ { role: "user", content: `Mark task ${id} as ${task.status}` },
3488
+ {
3489
+ role: "assistant",
3490
+ content: `Task ${id} has been updated to status: ${task.status}. ${task.completed_at ? `Completed at: ${task.completed_at}` : ""}`.trim()
3491
+ }
3492
+ ]
3493
+ };
3494
+ }
3495
+ function taskToSearchExample(tasks, query) {
3496
+ const matched = tasks.filter((t) => t.title.toLowerCase().includes(query.toLowerCase())).slice(0, 5);
3497
+ return {
3498
+ messages: [
3499
+ { role: "system", content: SYSTEM_PROMPT },
3500
+ { role: "user", content: `Search tasks for: "${query}"` },
3501
+ {
3502
+ role: "assistant",
3503
+ content: matched.length > 0 ? `Found ${matched.length} task(s):
3504
+ ${matched.map((t) => `- [${t.short_id ?? t.id}] ${t.title} (${t.status})`).join(`
3505
+ `)}` : `No tasks found matching "${query}".`
3506
+ }
3507
+ ]
3508
+ };
3509
+ }
3510
+ var gatherTrainingData = async (options = {}) => {
3511
+ const allTasks = listTasks({});
3512
+ const filtered = options.since ? allTasks.filter((t) => new Date(t.created_at) >= options.since) : allTasks;
3513
+ const sorted = filtered.slice().sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
3514
+ const fetchSet = options.limit ? sorted.slice(0, options.limit * 2) : sorted;
3515
+ const examples = [];
3516
+ for (const task of fetchSet) {
3517
+ examples.push(taskToCreateExample(task));
3518
+ const statusEx = taskToStatusUpdateExample(task);
3519
+ if (statusEx)
3520
+ examples.push(statusEx);
3521
+ }
3522
+ const searchTerms = ["urgent", "fix", "implement", "create", "update", "review"];
3523
+ for (const term of searchTerms) {
3524
+ examples.push(taskToSearchExample(sorted, term));
3525
+ }
3526
+ const finalExamples = options.limit ? examples.slice(0, options.limit) : examples;
3527
+ return {
3528
+ source: "todos",
3529
+ examples: finalExamples,
3530
+ count: finalExamples.length
3531
+ };
3532
+ };
3533
+ // src/lib/model-config.ts
3534
+ import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
3535
+ import { homedir } from "os";
3536
+ import { join as join4 } from "path";
3537
+ var DEFAULT_MODEL = "gpt-4o-mini";
3538
+ var CONFIG_DIR = join4(homedir(), ".todos");
3539
+ var CONFIG_PATH = join4(CONFIG_DIR, "config.json");
3540
+ function readConfig() {
3541
+ if (!existsSync4(CONFIG_PATH))
3542
+ return {};
3543
+ try {
3544
+ const raw = readFileSync2(CONFIG_PATH, "utf-8");
3545
+ return JSON.parse(raw);
3546
+ } catch {
3547
+ return {};
3548
+ }
3549
+ }
3550
+ function writeConfig(config) {
3551
+ if (!existsSync4(CONFIG_DIR)) {
3552
+ mkdirSync3(CONFIG_DIR, { recursive: true });
3553
+ }
3554
+ writeFileSync2(CONFIG_PATH, JSON.stringify(config, null, 2) + `
3555
+ `, "utf-8");
3556
+ }
3557
+ function getActiveModel() {
3558
+ const config = readConfig();
3559
+ return config.activeModel ?? DEFAULT_MODEL;
3560
+ }
3561
+ function setActiveModel(modelId) {
3562
+ const config = readConfig();
3563
+ config.activeModel = modelId;
3564
+ writeConfig(config);
3565
+ }
3566
+ function clearActiveModel() {
3567
+ const config = readConfig();
3568
+ delete config.activeModel;
3569
+ writeConfig(config);
3570
+ }
3333
3571
  // src/db/handoffs.ts
3334
3572
  function createHandoff(input, db) {
3335
3573
  const d = db || getDatabase();
@@ -4161,16 +4399,16 @@ function searchTasks(options, projectId, taskListId, db) {
4161
4399
  return rows.map(rowToTask3);
4162
4400
  }
4163
4401
  // src/lib/claude-tasks.ts
4164
- import { existsSync as existsSync4, readFileSync as readFileSync2, readdirSync as readdirSync2, writeFileSync as writeFileSync2 } from "fs";
4165
- import { join as join4 } from "path";
4402
+ import { existsSync as existsSync5, readFileSync as readFileSync3, readdirSync as readdirSync2, writeFileSync as writeFileSync3 } from "fs";
4403
+ import { join as join5 } from "path";
4166
4404
  function getTaskListDir(taskListId) {
4167
- return join4(HOME, ".claude", "tasks", taskListId);
4405
+ return join5(HOME, ".claude", "tasks", taskListId);
4168
4406
  }
4169
4407
  function readClaudeTask(dir, filename) {
4170
- return readJsonFile(join4(dir, filename));
4408
+ return readJsonFile(join5(dir, filename));
4171
4409
  }
4172
4410
  function writeClaudeTask(dir, task) {
4173
- writeJsonFile(join4(dir, `${task.id}.json`), task);
4411
+ writeJsonFile(join5(dir, `${task.id}.json`), task);
4174
4412
  }
4175
4413
  function toClaudeStatus(status) {
4176
4414
  if (status === "pending" || status === "in_progress" || status === "completed") {
@@ -4182,14 +4420,14 @@ function toSqliteStatus(status) {
4182
4420
  return status;
4183
4421
  }
4184
4422
  function readPrefixCounter(dir) {
4185
- const path = join4(dir, ".prefix-counter");
4186
- if (!existsSync4(path))
4423
+ const path = join5(dir, ".prefix-counter");
4424
+ if (!existsSync5(path))
4187
4425
  return 0;
4188
- const val = parseInt(readFileSync2(path, "utf-8").trim(), 10);
4426
+ const val = parseInt(readFileSync3(path, "utf-8").trim(), 10);
4189
4427
  return isNaN(val) ? 0 : val;
4190
4428
  }
4191
4429
  function writePrefixCounter(dir, value) {
4192
- writeFileSync2(join4(dir, ".prefix-counter"), String(value));
4430
+ writeFileSync3(join5(dir, ".prefix-counter"), String(value));
4193
4431
  }
4194
4432
  function formatPrefixedSubject(title, prefix, counter) {
4195
4433
  const padded = String(counter).padStart(5, "0");
@@ -4216,7 +4454,7 @@ function taskToClaudeTask(task, claudeTaskId, existingMeta) {
4216
4454
  }
4217
4455
  function pushToClaudeTaskList(taskListId, projectId, options = {}) {
4218
4456
  const dir = getTaskListDir(taskListId);
4219
- if (!existsSync4(dir))
4457
+ if (!existsSync5(dir))
4220
4458
  ensureDir2(dir);
4221
4459
  const filter = {};
4222
4460
  if (projectId)
@@ -4225,7 +4463,7 @@ function pushToClaudeTaskList(taskListId, projectId, options = {}) {
4225
4463
  const existingByTodosId = new Map;
4226
4464
  const files = listJsonFiles(dir);
4227
4465
  for (const f of files) {
4228
- const path = join4(dir, f);
4466
+ const path = join5(dir, f);
4229
4467
  const ct = readClaudeTask(dir, f);
4230
4468
  if (ct?.metadata?.["todos_id"]) {
4231
4469
  existingByTodosId.set(ct.metadata["todos_id"], { task: ct, mtimeMs: getFileMtimeMs(path) });
@@ -4312,7 +4550,7 @@ function pushToClaudeTaskList(taskListId, projectId, options = {}) {
4312
4550
  }
4313
4551
  function pullFromClaudeTaskList(taskListId, projectId, options = {}) {
4314
4552
  const dir = getTaskListDir(taskListId);
4315
- if (!existsSync4(dir)) {
4553
+ if (!existsSync5(dir)) {
4316
4554
  return { pushed: 0, pulled: 0, errors: [`Task list directory not found: ${dir}`] };
4317
4555
  }
4318
4556
  const files = readdirSync2(dir).filter((f) => f.endsWith(".json"));
@@ -4332,7 +4570,7 @@ function pullFromClaudeTaskList(taskListId, projectId, options = {}) {
4332
4570
  }
4333
4571
  for (const f of files) {
4334
4572
  try {
4335
- const filePath = join4(dir, f);
4573
+ const filePath = join5(dir, f);
4336
4574
  const ct = readClaudeTask(dir, f);
4337
4575
  if (!ct)
4338
4576
  continue;
@@ -4400,20 +4638,20 @@ function syncClaudeTaskList(taskListId, projectId, options = {}) {
4400
4638
  }
4401
4639
 
4402
4640
  // src/lib/agent-tasks.ts
4403
- import { existsSync as existsSync5 } from "fs";
4404
- import { join as join5 } from "path";
4641
+ import { existsSync as existsSync6 } from "fs";
4642
+ import { join as join6 } from "path";
4405
4643
  function agentBaseDir(agent) {
4406
4644
  const key = `TODOS_${agent.toUpperCase()}_TASKS_DIR`;
4407
- return process.env[key] || getAgentTasksDir(agent) || process.env["TODOS_AGENT_TASKS_DIR"] || join5(HOME, ".todos", "agents");
4645
+ return process.env[key] || getAgentTasksDir(agent) || process.env["TODOS_AGENT_TASKS_DIR"] || join6(HOME, ".todos", "agents");
4408
4646
  }
4409
4647
  function getTaskListDir2(agent, taskListId) {
4410
- return join5(agentBaseDir(agent), agent, taskListId);
4648
+ return join6(agentBaseDir(agent), agent, taskListId);
4411
4649
  }
4412
4650
  function readAgentTask(dir, filename) {
4413
- return readJsonFile(join5(dir, filename));
4651
+ return readJsonFile(join6(dir, filename));
4414
4652
  }
4415
4653
  function writeAgentTask(dir, task) {
4416
- writeJsonFile(join5(dir, `${task.id}.json`), task);
4654
+ writeJsonFile(join6(dir, `${task.id}.json`), task);
4417
4655
  }
4418
4656
  function taskToAgentTask(task, externalId, existingMeta) {
4419
4657
  return {
@@ -4438,7 +4676,7 @@ function metadataKey(agent) {
4438
4676
  }
4439
4677
  function pushToAgentTaskList(agent, taskListId, projectId, options = {}) {
4440
4678
  const dir = getTaskListDir2(agent, taskListId);
4441
- if (!existsSync5(dir))
4679
+ if (!existsSync6(dir))
4442
4680
  ensureDir2(dir);
4443
4681
  const filter = {};
4444
4682
  if (projectId)
@@ -4447,7 +4685,7 @@ function pushToAgentTaskList(agent, taskListId, projectId, options = {}) {
4447
4685
  const existingByTodosId = new Map;
4448
4686
  const files = listJsonFiles(dir);
4449
4687
  for (const f of files) {
4450
- const path = join5(dir, f);
4688
+ const path = join6(dir, f);
4451
4689
  const at = readAgentTask(dir, f);
4452
4690
  if (at?.metadata?.["todos_id"]) {
4453
4691
  existingByTodosId.set(at.metadata["todos_id"], { task: at, mtimeMs: getFileMtimeMs(path) });
@@ -4521,7 +4759,7 @@ function pushToAgentTaskList(agent, taskListId, projectId, options = {}) {
4521
4759
  }
4522
4760
  function pullFromAgentTaskList(agent, taskListId, projectId, options = {}) {
4523
4761
  const dir = getTaskListDir2(agent, taskListId);
4524
- if (!existsSync5(dir)) {
4762
+ if (!existsSync6(dir)) {
4525
4763
  return { pushed: 0, pulled: 0, errors: [`Task list directory not found: ${dir}`] };
4526
4764
  }
4527
4765
  const files = listJsonFiles(dir);
@@ -4540,7 +4778,7 @@ function pullFromAgentTaskList(agent, taskListId, projectId, options = {}) {
4540
4778
  }
4541
4779
  for (const f of files) {
4542
4780
  try {
4543
- const filePath = join5(dir, f);
4781
+ const filePath = join6(dir, f);
4544
4782
  const at = readAgentTask(dir, f);
4545
4783
  if (!at)
4546
4784
  continue;
@@ -4677,8 +4915,8 @@ function syncWithAgents(agents, taskListIdByAgent, projectId, direction = "both"
4677
4915
  return { pushed, pulled, errors };
4678
4916
  }
4679
4917
  // src/lib/extract.ts
4680
- import { readFileSync as readFileSync3, statSync as statSync2 } from "fs";
4681
- import { relative, resolve as resolve2, join as join6 } from "path";
4918
+ import { readFileSync as readFileSync4, statSync as statSync2 } from "fs";
4919
+ import { relative, resolve as resolve2, join as join7 } from "path";
4682
4920
  var EXTRACT_TAGS = ["TODO", "FIXME", "HACK", "XXX", "BUG", "NOTE"];
4683
4921
  var DEFAULT_EXTENSIONS = new Set([
4684
4922
  ".ts",
@@ -4812,9 +5050,9 @@ function extractTodos(options, db) {
4812
5050
  const files = collectFiles(basePath, extensions);
4813
5051
  const allComments = [];
4814
5052
  for (const file of files) {
4815
- const fullPath = statSync2(basePath).isFile() ? basePath : join6(basePath, file);
5053
+ const fullPath = statSync2(basePath).isFile() ? basePath : join7(basePath, file);
4816
5054
  try {
4817
- const source = readFileSync3(fullPath, "utf-8");
5055
+ const source = readFileSync4(fullPath, "utf-8");
4818
5056
  const relPath = statSync2(basePath).isFile() ? relative(resolve2(basePath, ".."), fullPath) : file;
4819
5057
  const comments = extractFromSource(source, relPath, tags);
4820
5058
  allComments.push(...comments);
@@ -4998,6 +5236,188 @@ function issueToTask(issue, opts) {
4998
5236
  agent_id: opts?.agent_id
4999
5237
  };
5000
5238
  }
5239
+ // src/db/traces.ts
5240
+ function logTrace(input, db) {
5241
+ const d = db || getDatabase();
5242
+ const id = uuid();
5243
+ const timestamp = now();
5244
+ 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)
5245
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
5246
+ id,
5247
+ input.task_id,
5248
+ input.agent_id || null,
5249
+ input.trace_type,
5250
+ input.name || null,
5251
+ input.input_summary || null,
5252
+ input.output_summary || null,
5253
+ input.duration_ms ?? null,
5254
+ input.tokens ?? null,
5255
+ input.cost_usd ?? null,
5256
+ timestamp
5257
+ ]);
5258
+ return {
5259
+ id,
5260
+ task_id: input.task_id,
5261
+ agent_id: input.agent_id || null,
5262
+ trace_type: input.trace_type,
5263
+ name: input.name || null,
5264
+ input_summary: input.input_summary || null,
5265
+ output_summary: input.output_summary || null,
5266
+ duration_ms: input.duration_ms ?? null,
5267
+ tokens: input.tokens ?? null,
5268
+ cost_usd: input.cost_usd ?? null,
5269
+ created_at: timestamp
5270
+ };
5271
+ }
5272
+ function getTaskTraces(taskId, db) {
5273
+ const d = db || getDatabase();
5274
+ return d.query("SELECT * FROM task_traces WHERE task_id = ? ORDER BY created_at DESC").all(taskId);
5275
+ }
5276
+ function getTraceStats(taskId, db) {
5277
+ const d = db || getDatabase();
5278
+ const row = d.query(`SELECT COUNT(*) as total,
5279
+ SUM(CASE WHEN trace_type = 'tool_call' THEN 1 ELSE 0 END) as tool_calls,
5280
+ SUM(CASE WHEN trace_type = 'llm_call' THEN 1 ELSE 0 END) as llm_calls,
5281
+ SUM(CASE WHEN trace_type = 'error' THEN 1 ELSE 0 END) as errors,
5282
+ COALESCE(SUM(tokens), 0) as total_tokens,
5283
+ COALESCE(SUM(cost_usd), 0) as total_cost_usd,
5284
+ COALESCE(SUM(duration_ms), 0) as total_duration_ms
5285
+ FROM task_traces WHERE task_id = ?`).get(taskId);
5286
+ return row;
5287
+ }
5288
+ // src/db/snapshots.ts
5289
+ function rowToSnapshot(row) {
5290
+ return {
5291
+ ...row,
5292
+ snapshot_type: row.snapshot_type,
5293
+ files_open: JSON.parse(row.files_open || "[]"),
5294
+ attempts: JSON.parse(row.attempts || "[]"),
5295
+ blockers: JSON.parse(row.blockers || "[]"),
5296
+ metadata: JSON.parse(row.metadata || "{}")
5297
+ };
5298
+ }
5299
+ function saveSnapshot(input, db) {
5300
+ const d = db || getDatabase();
5301
+ const id = uuid();
5302
+ const timestamp = now();
5303
+ 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)
5304
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
5305
+ id,
5306
+ input.agent_id || null,
5307
+ input.task_id || null,
5308
+ input.project_id || null,
5309
+ input.snapshot_type,
5310
+ input.plan_summary || null,
5311
+ JSON.stringify(input.files_open || []),
5312
+ JSON.stringify(input.attempts || []),
5313
+ JSON.stringify(input.blockers || []),
5314
+ input.next_steps || null,
5315
+ JSON.stringify(input.metadata || {}),
5316
+ timestamp
5317
+ ]);
5318
+ return {
5319
+ id,
5320
+ agent_id: input.agent_id || null,
5321
+ task_id: input.task_id || null,
5322
+ project_id: input.project_id || null,
5323
+ snapshot_type: input.snapshot_type,
5324
+ plan_summary: input.plan_summary || null,
5325
+ files_open: input.files_open || [],
5326
+ attempts: input.attempts || [],
5327
+ blockers: input.blockers || [],
5328
+ next_steps: input.next_steps || null,
5329
+ metadata: input.metadata || {},
5330
+ created_at: timestamp
5331
+ };
5332
+ }
5333
+ function getLatestSnapshot(agentId, taskId, db) {
5334
+ const d = db || getDatabase();
5335
+ const conditions = [];
5336
+ const params = [];
5337
+ if (agentId) {
5338
+ conditions.push("agent_id = ?");
5339
+ params.push(agentId);
5340
+ }
5341
+ if (taskId) {
5342
+ conditions.push("task_id = ?");
5343
+ params.push(taskId);
5344
+ }
5345
+ if (conditions.length === 0)
5346
+ return null;
5347
+ const where = conditions.join(" AND ");
5348
+ const row = d.query(`SELECT * FROM context_snapshots WHERE ${where} ORDER BY created_at DESC LIMIT 1`).get(...params);
5349
+ return row ? rowToSnapshot(row) : null;
5350
+ }
5351
+ function listSnapshots(opts, db) {
5352
+ const d = db || getDatabase();
5353
+ const conditions = [];
5354
+ const params = [];
5355
+ if (opts.agent_id) {
5356
+ conditions.push("agent_id = ?");
5357
+ params.push(opts.agent_id);
5358
+ }
5359
+ if (opts.task_id) {
5360
+ conditions.push("task_id = ?");
5361
+ params.push(opts.task_id);
5362
+ }
5363
+ if (opts.project_id) {
5364
+ conditions.push("project_id = ?");
5365
+ params.push(opts.project_id);
5366
+ }
5367
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
5368
+ const limit = opts.limit || 20;
5369
+ params.push(limit);
5370
+ return d.query(`SELECT * FROM context_snapshots ${where} ORDER BY created_at DESC LIMIT ?`).all(...params).map(rowToSnapshot);
5371
+ }
5372
+ // src/db/budgets.ts
5373
+ function setBudget(agentId, opts, db) {
5374
+ const d = db || getDatabase();
5375
+ const timestamp = now();
5376
+ d.run(`INSERT INTO agent_budgets (agent_id, max_concurrent, max_cost_usd, max_task_minutes, period_hours, created_at, updated_at)
5377
+ VALUES (?, ?, ?, ?, ?, ?, ?)
5378
+ ON CONFLICT(agent_id) DO UPDATE SET
5379
+ max_concurrent = COALESCE(?, max_concurrent),
5380
+ max_cost_usd = COALESCE(?, max_cost_usd),
5381
+ max_task_minutes = COALESCE(?, max_task_minutes),
5382
+ period_hours = COALESCE(?, period_hours),
5383
+ updated_at = ?`, [
5384
+ agentId,
5385
+ opts.max_concurrent ?? 5,
5386
+ opts.max_cost_usd ?? null,
5387
+ opts.max_task_minutes ?? null,
5388
+ opts.period_hours ?? 24,
5389
+ timestamp,
5390
+ timestamp,
5391
+ opts.max_concurrent ?? null,
5392
+ opts.max_cost_usd ?? null,
5393
+ opts.max_task_minutes ?? null,
5394
+ opts.period_hours ?? null,
5395
+ timestamp
5396
+ ]);
5397
+ return getBudget(agentId, d);
5398
+ }
5399
+ function getBudget(agentId, db) {
5400
+ const d = db || getDatabase();
5401
+ return d.query("SELECT * FROM agent_budgets WHERE agent_id = ?").get(agentId);
5402
+ }
5403
+ function checkBudget(agentId, db) {
5404
+ const d = db || getDatabase();
5405
+ const budget = getBudget(agentId, d);
5406
+ if (!budget)
5407
+ return { allowed: true, current_concurrent: 0, max_concurrent: 999 };
5408
+ const concurrent = countTasks({ status: "in_progress", assigned_to: agentId }, d);
5409
+ if (concurrent >= budget.max_concurrent) {
5410
+ return { allowed: false, reason: `Concurrent limit reached (${concurrent}/${budget.max_concurrent})`, current_concurrent: concurrent, max_concurrent: budget.max_concurrent };
5411
+ }
5412
+ if (budget.max_cost_usd != null) {
5413
+ const periodStart = new Date(Date.now() - budget.period_hours * 60 * 60 * 1000).toISOString();
5414
+ 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);
5415
+ if (costRow.total >= budget.max_cost_usd) {
5416
+ 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 };
5417
+ }
5418
+ }
5419
+ return { allowed: true, current_concurrent: concurrent, max_concurrent: budget.max_concurrent };
5420
+ }
5001
5421
  export {
5002
5422
  uuid,
5003
5423
  updateTaskList,
@@ -5017,12 +5437,16 @@ export {
5017
5437
  syncWithAgents,
5018
5438
  syncWithAgent,
5019
5439
  syncKgEdges,
5440
+ stealTask,
5020
5441
  startTask,
5021
5442
  slugify,
5022
5443
  setTaskStatus,
5023
5444
  setTaskPriority,
5445
+ setBudget,
5446
+ setActiveModel,
5024
5447
  searchTasks,
5025
5448
  scoreTask,
5449
+ saveSnapshot,
5026
5450
  resolvePartialId,
5027
5451
  resetDatabase,
5028
5452
  removeTaskRelationshipByPair,
@@ -5044,8 +5468,10 @@ export {
5044
5468
  nextOccurrence,
5045
5469
  moveTask,
5046
5470
  matchCapabilities,
5471
+ logTrace,
5047
5472
  logTaskChange,
5048
5473
  logProgress,
5474
+ logCost,
5049
5475
  lockTask,
5050
5476
  loadConfig,
5051
5477
  listWebhooks,
@@ -5053,6 +5479,7 @@ export {
5053
5479
  listTasks,
5054
5480
  listTaskLists,
5055
5481
  listTaskFiles,
5482
+ listSnapshots,
5056
5483
  listSessions,
5057
5484
  listProjects,
5058
5485
  listProjectSources,
@@ -5065,9 +5492,11 @@ export {
5065
5492
  isValidRecurrenceRule,
5066
5493
  isAgentConflict,
5067
5494
  getWebhook,
5495
+ getTraceStats,
5068
5496
  getTemplate,
5069
5497
  getTasksChangedSince,
5070
5498
  getTaskWithRelations,
5499
+ getTaskTraces,
5071
5500
  getTaskStats,
5072
5501
  getTaskRelationships,
5073
5502
  getTaskRelationship,
@@ -5096,6 +5525,7 @@ export {
5096
5525
  getOrg,
5097
5526
  getNextTask,
5098
5527
  getLeaderboard,
5528
+ getLatestSnapshot,
5099
5529
  getLatestHandoff,
5100
5530
  getImpactAnalysis,
5101
5531
  getDirectReports,
@@ -5107,11 +5537,14 @@ export {
5107
5537
  getChecklist,
5108
5538
  getCapableAgents,
5109
5539
  getBurndown,
5540
+ getBudget,
5110
5541
  getBlockingDeps,
5111
5542
  getAgentMetrics,
5112
5543
  getAgentByName,
5113
5544
  getAgent,
5114
5545
  getActiveWork,
5546
+ getActiveModel,
5547
+ gatherTrainingData,
5115
5548
  findTasksByFile,
5116
5549
  findRelatedTaskIds,
5117
5550
  findPath,
@@ -5149,11 +5582,14 @@ export {
5149
5582
  closeDatabase,
5150
5583
  cloneTask,
5151
5584
  clearChecklist,
5585
+ clearActiveModel,
5152
5586
  cleanExpiredLocks,
5587
+ claimOrSteal,
5153
5588
  claimNextTask,
5154
5589
  checkLock,
5155
5590
  checkCompletionGuard,
5156
5591
  checkChecklistItem,
5592
+ checkBudget,
5157
5593
  bulkUpdateTasks,
5158
5594
  bulkCreateTasks,
5159
5595
  bulkAddTaskFiles,
@@ -5181,6 +5617,7 @@ export {
5181
5617
  LockError,
5182
5618
  EXTRACT_TAGS,
5183
5619
  DependencyCycleError,
5620
+ DEFAULT_MODEL,
5184
5621
  CompletionGuardError,
5185
5622
  AgentNotFoundError
5186
5623
  };