aitasks 1.4.0 → 1.4.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.
Files changed (2) hide show
  1. package/dist/index.js +282 -142
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1890,7 +1890,7 @@ var require_commander = __commonJS((exports) => {
1890
1890
  var require_package = __commonJS((exports, module) => {
1891
1891
  module.exports = {
1892
1892
  name: "aitasks",
1893
- version: "1.4.0",
1893
+ version: "1.4.2",
1894
1894
  description: "CLI task management tool built for AI agents",
1895
1895
  type: "module",
1896
1896
  bin: {
@@ -29071,7 +29071,7 @@ import { join as join3 } from "path";
29071
29071
  import { existsSync as existsSync3 } from "fs";
29072
29072
 
29073
29073
  // src/db/schema.ts
29074
- var SCHEMA_VERSION = 3;
29074
+ var SCHEMA_VERSION = 4;
29075
29075
  function runMigrations(db) {
29076
29076
  const row = db.query(`SELECT value FROM meta WHERE key = 'schema_version'`).get();
29077
29077
  const current = row ? parseInt(row.value, 10) : 1;
@@ -29085,6 +29085,12 @@ function runMigrations(db) {
29085
29085
  db.exec(`UPDATE agents SET first_seen = last_seen WHERE first_seen IS NULL`);
29086
29086
  db.exec(`UPDATE meta SET value = '3' WHERE key = 'schema_version'`);
29087
29087
  }
29088
+ if (current < 4) {
29089
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_events_task_created ON events(task_id, created_at)`);
29090
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_agents_current_task ON agents(current_task)`);
29091
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_tasks_type ON tasks(type)`);
29092
+ db.exec(`UPDATE meta SET value = '4' WHERE key = 'schema_version'`);
29093
+ }
29088
29094
  }
29089
29095
  function initializeSchema(db) {
29090
29096
  db.exec(`
@@ -29134,8 +29140,11 @@ function initializeSchema(db) {
29134
29140
  CREATE INDEX IF NOT EXISTS idx_tasks_priority ON tasks(priority);
29135
29141
  CREATE INDEX IF NOT EXISTS idx_tasks_assigned_to ON tasks(assigned_to);
29136
29142
  CREATE INDEX IF NOT EXISTS idx_tasks_parent_id ON tasks(parent_id);
29137
- CREATE INDEX IF NOT EXISTS idx_events_task_id ON events(task_id);
29138
- CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at);
29143
+ CREATE INDEX IF NOT EXISTS idx_events_task_id ON events(task_id);
29144
+ CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at);
29145
+ CREATE INDEX IF NOT EXISTS idx_events_task_created ON events(task_id, created_at);
29146
+ CREATE INDEX IF NOT EXISTS idx_agents_current_task ON agents(current_task);
29147
+ CREATE INDEX IF NOT EXISTS idx_tasks_type ON tasks(type);
29139
29148
  `);
29140
29149
  const insert = db.prepare(`INSERT OR IGNORE INTO meta (key, value) VALUES (?, ?)`);
29141
29150
  insert.run("last_task_number", "0");
@@ -30214,29 +30223,82 @@ var be = async (s, n) => {
30214
30223
  };
30215
30224
 
30216
30225
  // src/models/event.ts
30217
- function logEvent(data) {
30226
+ var _stmtDb = null;
30227
+ var _insertEvent = null;
30228
+ var _selectTaskEvents = null;
30229
+ var _selectAllEvents = null;
30230
+ var _selectLastReviewEvent = null;
30231
+ function stmts() {
30218
30232
  const db = getDb();
30219
- db.run(`INSERT INTO events (task_id, agent_id, event_type, payload, created_at)
30220
- VALUES (?, ?, ?, ?, ?)`, [
30221
- data.task_id,
30222
- data.agent_id ?? null,
30223
- data.event_type,
30224
- JSON.stringify(data.payload ?? {}),
30225
- Date.now()
30226
- ]);
30233
+ if (_stmtDb !== db) {
30234
+ _stmtDb = db;
30235
+ _insertEvent = db.query(`INSERT INTO events (task_id, agent_id, event_type, payload, created_at)
30236
+ VALUES (?, ?, ?, ?, ?)`);
30237
+ _selectTaskEvents = db.query("SELECT * FROM events WHERE task_id = ? ORDER BY created_at ASC");
30238
+ _selectAllEvents = db.query("SELECT * FROM events ORDER BY created_at DESC LIMIT ?");
30239
+ _selectLastReviewEvent = db.query(`SELECT * FROM events WHERE task_id = ? AND event_type = 'review_requested'
30240
+ ORDER BY created_at DESC LIMIT 1`);
30241
+ }
30242
+ return { _insertEvent, _selectTaskEvents, _selectAllEvents, _selectLastReviewEvent };
30243
+ }
30244
+ function parseEvent(r2) {
30245
+ return { ...r2, payload: JSON.parse(r2.payload) };
30246
+ }
30247
+ function logEvent(data) {
30248
+ stmts()._insertEvent.run(data.task_id, data.agent_id ?? null, data.event_type, JSON.stringify(data.payload ?? {}), Date.now());
30227
30249
  }
30228
30250
  function getTaskEvents(taskId) {
30229
- const db = getDb();
30230
- const rows = db.query("SELECT * FROM events WHERE task_id = ? ORDER BY created_at ASC").all(taskId);
30231
- return rows.map((r2) => ({ ...r2, payload: JSON.parse(r2.payload) }));
30251
+ const rows = stmts()._selectTaskEvents.all(taskId);
30252
+ return rows.map(parseEvent);
30253
+ }
30254
+ function getLastReviewEvent(taskId) {
30255
+ const row = stmts()._selectLastReviewEvent.get(taskId);
30256
+ return row ? parseEvent(row) : null;
30232
30257
  }
30233
30258
  function getAllEvents(limit = 100) {
30234
- const db = getDb();
30235
- const rows = db.query("SELECT * FROM events ORDER BY created_at DESC LIMIT ?").all(limit);
30236
- return rows.map((r2) => ({ ...r2, payload: JSON.parse(r2.payload) }));
30259
+ const rows = stmts()._selectAllEvents.all(limit);
30260
+ return rows.map(parseEvent);
30237
30261
  }
30238
30262
 
30239
30263
  // src/models/task.ts
30264
+ var _stmtDb2 = null;
30265
+ var _getTaskStmt = null;
30266
+ var _getAllIdsStmt = null;
30267
+ var _getMetaStmt = null;
30268
+ var _updateMetaStmt = null;
30269
+ var _upsertAgentStmt = null;
30270
+ var _heartbeatStmt = null;
30271
+ var _releaseAgentStmt = null;
30272
+ var _subtaskCountStmt = null;
30273
+ function stmts2() {
30274
+ const db = getDb();
30275
+ if (_stmtDb2 !== db) {
30276
+ _stmtDb2 = db;
30277
+ _getTaskStmt = db.query("SELECT * FROM tasks WHERE id = ?");
30278
+ _getAllIdsStmt = db.query("SELECT id FROM tasks");
30279
+ _getMetaStmt = db.query(`SELECT value FROM meta WHERE key = 'last_task_number'`);
30280
+ _updateMetaStmt = db.query(`UPDATE meta SET value = ? WHERE key = 'last_task_number'`);
30281
+ _upsertAgentStmt = db.query(`INSERT INTO agents (id, first_seen, last_seen, current_task)
30282
+ VALUES (?, ?, ?, ?)
30283
+ ON CONFLICT(id) DO UPDATE SET last_seen = excluded.last_seen, current_task = excluded.current_task`);
30284
+ _heartbeatStmt = db.query(`INSERT INTO agents (id, first_seen, last_seen, current_task)
30285
+ VALUES (?, ?, ?, ?)
30286
+ ON CONFLICT(id) DO UPDATE SET last_seen = excluded.last_seen`);
30287
+ _releaseAgentStmt = db.query(`UPDATE agents SET current_task = NULL WHERE id = ?`);
30288
+ _subtaskCountStmt = db.query(`SELECT COUNT(*) as count FROM tasks WHERE parent_id = ?`);
30289
+ }
30290
+ return {
30291
+ db,
30292
+ getTask: _getTaskStmt,
30293
+ getAllIds: _getAllIdsStmt,
30294
+ getMeta: _getMetaStmt,
30295
+ updateMeta: _updateMetaStmt,
30296
+ upsertAgent: _upsertAgentStmt,
30297
+ heartbeat: _heartbeatStmt,
30298
+ releaseAgent: _releaseAgentStmt,
30299
+ subtaskCount: _subtaskCountStmt
30300
+ };
30301
+ }
30240
30302
  function parseTask(row) {
30241
30303
  return {
30242
30304
  id: row.id,
@@ -30260,17 +30322,20 @@ function parseTask(row) {
30260
30322
  };
30261
30323
  }
30262
30324
  function nextTaskId() {
30263
- const db = getDb();
30264
- const meta = db.query(`SELECT value FROM meta WHERE key = 'last_task_number'`).get();
30325
+ const s = stmts2();
30326
+ const meta = s.getMeta.get();
30265
30327
  const next = parseInt(meta.value, 10) + 1;
30266
- db.run(`UPDATE meta SET value = ? WHERE key = 'last_task_number'`, [String(next)]);
30328
+ s.updateMeta.run(String(next));
30267
30329
  return `TASK-${String(next).padStart(3, "0")}`;
30268
30330
  }
30269
30331
  function getTask(id) {
30270
- const db = getDb();
30271
- const row = db.query("SELECT * FROM tasks WHERE id = ?").get(id);
30332
+ const row = stmts2().getTask.get(id);
30272
30333
  return row ? parseTask(row) : null;
30273
30334
  }
30335
+ function getAllTaskIds() {
30336
+ const rows = stmts2().getAllIds.all();
30337
+ return rows.map((r2) => r2.id);
30338
+ }
30274
30339
  var PRIORITY_ORDER = `CASE priority
30275
30340
  WHEN 'critical' THEN 0
30276
30341
  WHEN 'high' THEN 1
@@ -30284,7 +30349,7 @@ var STATUS_ORDER = `CASE status
30284
30349
  WHEN 'backlog' THEN 4
30285
30350
  WHEN 'done' THEN 5 END`;
30286
30351
  function listTasks(filters = {}) {
30287
- const db = getDb();
30352
+ const db = stmts2().db;
30288
30353
  const conds = [];
30289
30354
  const params = [];
30290
30355
  if (filters.status) {
@@ -30329,8 +30394,25 @@ function listTasks(filters = {}) {
30329
30394
  function getSubtasks(parentId) {
30330
30395
  return listTasks({ parent_id: parentId });
30331
30396
  }
30397
+ function searchTasks(terms, status) {
30398
+ const db = stmts2().db;
30399
+ const conds = [];
30400
+ const params = [];
30401
+ if (status) {
30402
+ conds.push("status = ?");
30403
+ params.push(status);
30404
+ }
30405
+ for (const term of terms) {
30406
+ conds.push(`(title LIKE ? OR description LIKE ? OR implementation_notes LIKE ? OR acceptance_criteria LIKE ?)`);
30407
+ const like = `%${term}%`;
30408
+ params.push(like, like, like, like);
30409
+ }
30410
+ const where = conds.length > 0 ? `WHERE ${conds.join(" AND ")}` : "";
30411
+ const rows = db.query(`SELECT * FROM tasks ${where}`).all(...params);
30412
+ return rows.map(parseTask);
30413
+ }
30332
30414
  function createTask(data) {
30333
- const db = getDb();
30415
+ const db = stmts2().db;
30334
30416
  const id = nextTaskId();
30335
30417
  const now = Date.now();
30336
30418
  db.run(`INSERT INTO tasks
@@ -30361,7 +30443,7 @@ var JSON_FIELDS = new Set([
30361
30443
  "metadata"
30362
30444
  ]);
30363
30445
  function updateTask(id, updates) {
30364
- const db = getDb();
30446
+ const db = stmts2().db;
30365
30447
  const fields = ["updated_at = ?"];
30366
30448
  const params = [Date.now()];
30367
30449
  for (const [key, value] of Object.entries(updates)) {
@@ -30371,8 +30453,8 @@ function updateTask(id, updates) {
30371
30453
  params.push(JSON_FIELDS.has(key) ? JSON.stringify(value) : value);
30372
30454
  }
30373
30455
  params.push(id);
30374
- db.run(`UPDATE tasks SET ${fields.join(", ")} WHERE id = ?`, params);
30375
- return getTask(id);
30456
+ const row = db.query(`UPDATE tasks SET ${fields.join(", ")} WHERE id = ? RETURNING *`).get(...params);
30457
+ return row ? parseTask(row) : null;
30376
30458
  }
30377
30459
  function addImplementationNote(taskId, note, agentId) {
30378
30460
  const task = getTask(taskId);
@@ -30420,7 +30502,7 @@ function checkCriterion(taskId, index, evidence, agentId) {
30420
30502
  return { task: updated };
30421
30503
  }
30422
30504
  function claimTask(taskId, agentId) {
30423
- const db = getDb();
30505
+ const s = stmts2();
30424
30506
  const task = getTask(taskId);
30425
30507
  if (!task)
30426
30508
  return { task: null, error: "Task not found" };
@@ -30435,14 +30517,14 @@ function claimTask(taskId, agentId) {
30435
30517
  error: `Task is blocked by: ${task.blocked_by.join(", ")}. Complete those first.`
30436
30518
  };
30437
30519
  }
30438
- db.run(`INSERT INTO agents (id, first_seen, last_seen, current_task)
30439
- VALUES (?, ?, ?, ?)
30440
- ON CONFLICT(id) DO UPDATE SET last_seen = excluded.last_seen, current_task = excluded.current_task`, [agentId, Date.now(), Date.now(), taskId]);
30520
+ const now = Date.now();
30521
+ s.upsertAgent.run(agentId, now, now, taskId);
30441
30522
  const updated = updateTask(taskId, { assigned_to: agentId, status: "ready" });
30442
30523
  logEvent({ task_id: taskId, agent_id: agentId, event_type: "claimed", payload: {} });
30443
30524
  return { task: updated };
30444
30525
  }
30445
30526
  function startTask(taskId, agentId) {
30527
+ const s = stmts2();
30446
30528
  const task = getTask(taskId);
30447
30529
  if (!task)
30448
30530
  return { task: null, error: "Task not found" };
@@ -30451,11 +30533,9 @@ function startTask(taskId, agentId) {
30451
30533
  if (task.assigned_to && task.assigned_to !== agentId) {
30452
30534
  return { task: null, error: `Task is assigned to ${task.assigned_to}, not ${agentId}` };
30453
30535
  }
30454
- const db = getDb();
30455
30536
  if (!task.assigned_to) {
30456
- db.run(`INSERT INTO agents (id, first_seen, last_seen, current_task)
30457
- VALUES (?, ?, ?, ?)
30458
- ON CONFLICT(id) DO UPDATE SET last_seen = excluded.last_seen, current_task = excluded.current_task`, [agentId, Date.now(), Date.now(), taskId]);
30537
+ const now = Date.now();
30538
+ s.upsertAgent.run(agentId, now, now, taskId);
30459
30539
  }
30460
30540
  const updated = updateTask(taskId, {
30461
30541
  assigned_to: agentId,
@@ -30466,7 +30546,7 @@ function startTask(taskId, agentId) {
30466
30546
  return { task: updated };
30467
30547
  }
30468
30548
  function completeTask(taskId, agentId) {
30469
- const db = getDb();
30549
+ const s = stmts2();
30470
30550
  const task = getTask(taskId);
30471
30551
  if (!task)
30472
30552
  return { task: null, error: "Task not found" };
@@ -30498,8 +30578,7 @@ function completeTask(taskId, agentId) {
30498
30578
  ` + ` A separate review sub-agent must run: aitasks done ${taskId} --agent <review-agent-id>`
30499
30579
  };
30500
30580
  }
30501
- const events = getTaskEvents(taskId);
30502
- const reviewEvent = [...events].reverse().find((e2) => e2.event_type === "review_requested");
30581
+ const reviewEvent = getLastReviewEvent(taskId);
30503
30582
  if (reviewEvent?.agent_id && reviewEvent.agent_id === agentId) {
30504
30583
  return {
30505
30584
  task: null,
@@ -30508,69 +30587,87 @@ function completeTask(taskId, agentId) {
30508
30587
  };
30509
30588
  }
30510
30589
  }
30511
- const updated = updateTask(taskId, { status: "done", completed_at: Date.now() });
30512
- logEvent({ task_id: taskId, agent_id: agentId, event_type: "completed", payload: {} });
30513
- const pendingRows = db.query(`SELECT id, blocked_by FROM tasks WHERE status != 'done'`).all();
30514
- for (const row of pendingRows) {
30515
- const blockedBy = JSON.parse(row.blocked_by);
30516
- if (!blockedBy.includes(taskId))
30517
- continue;
30518
- const remaining = blockedBy.filter((id) => id !== taskId);
30519
- const newStatus = remaining.length === 0 ? "ready" : "blocked";
30520
- db.run(`UPDATE tasks SET blocked_by = ?, status = ?, updated_at = ? WHERE id = ?`, [JSON.stringify(remaining), newStatus, Date.now(), row.id]);
30521
- if (newStatus === "ready") {
30522
- logEvent({
30523
- task_id: row.id,
30524
- event_type: "auto_unblocked",
30525
- payload: { unblocked_by: taskId }
30526
- });
30590
+ const db = s.db;
30591
+ const doComplete = db.transaction(() => {
30592
+ const updated2 = updateTask(taskId, { status: "done", completed_at: Date.now() });
30593
+ logEvent({ task_id: taskId, agent_id: agentId, event_type: "completed", payload: {} });
30594
+ const pendingRows = db.query(`SELECT id, blocked_by FROM tasks
30595
+ WHERE status != 'done' AND blocked_by LIKE '%' || ? || '%'`).all(taskId);
30596
+ for (const row of pendingRows) {
30597
+ const blockedBy = JSON.parse(row.blocked_by);
30598
+ if (!blockedBy.includes(taskId))
30599
+ continue;
30600
+ const remaining = blockedBy.filter((id) => id !== taskId);
30601
+ const newStatus = remaining.length === 0 ? "ready" : "blocked";
30602
+ db.run(`UPDATE tasks SET blocked_by = ?, status = ?, updated_at = ? WHERE id = ?`, [JSON.stringify(remaining), newStatus, Date.now(), row.id]);
30603
+ if (newStatus === "ready") {
30604
+ logEvent({
30605
+ task_id: row.id,
30606
+ event_type: "auto_unblocked",
30607
+ payload: { unblocked_by: taskId }
30608
+ });
30609
+ }
30527
30610
  }
30528
- }
30529
- if (agentId)
30530
- db.run(`UPDATE agents SET current_task = NULL WHERE id = ?`, [agentId]);
30611
+ if (agentId)
30612
+ s.releaseAgent.run(agentId);
30613
+ return updated2;
30614
+ });
30615
+ const updated = doComplete();
30531
30616
  return { task: updated };
30532
30617
  }
30533
30618
  function blockTask(taskId, blockerIds, agentId) {
30619
+ const s = stmts2();
30620
+ const db = s.db;
30534
30621
  const task = getTask(taskId);
30535
30622
  if (!task)
30536
30623
  return { task: null, error: "Task not found" };
30624
+ const placeholders = blockerIds.map(() => "?").join(",");
30625
+ const blockerRows = db.query(`SELECT * FROM tasks WHERE id IN (${placeholders})`).all(...blockerIds);
30626
+ const blockerMap = new Map(blockerRows.map((r2) => [r2.id, parseTask(r2)]));
30627
+ const allRows = db.query("SELECT * FROM tasks").all();
30628
+ const allTaskMap = new Map(allRows.map((r2) => [r2.id, parseTask(r2)]));
30537
30629
  for (const bid of blockerIds) {
30538
- const blocker = getTask(bid);
30630
+ const blocker = blockerMap.get(bid);
30539
30631
  if (!blocker)
30540
30632
  return { task: null, error: `Blocker not found: ${bid}` };
30541
30633
  if (blocker.status === "done") {
30542
30634
  return { task: null, error: `Cannot block on completed task: ${bid}` };
30543
30635
  }
30544
- if (detectCycle(taskId, bid)) {
30636
+ if (detectCycleFromMap(taskId, bid, allTaskMap)) {
30545
30637
  return { task: null, error: `Circular dependency detected: ${taskId} \u2192 ${bid}` };
30546
30638
  }
30547
30639
  }
30548
30640
  const newBlockedBy = [...new Set([...task.blocked_by, ...blockerIds])];
30549
- for (const bid of blockerIds) {
30550
- const blocker = getTask(bid);
30551
- updateTask(bid, { blocks: [...new Set([...blocker.blocks, taskId])] });
30552
- }
30553
- const updated = updateTask(taskId, { blocked_by: newBlockedBy, status: "blocked" });
30554
- logEvent({
30555
- task_id: taskId,
30556
- agent_id: agentId,
30557
- event_type: "blocked",
30558
- payload: { blocked_by: blockerIds }
30641
+ const doBlock = db.transaction(() => {
30642
+ for (const bid of blockerIds) {
30643
+ const blocker = blockerMap.get(bid);
30644
+ const newBlocks = JSON.stringify([...new Set([...blocker.blocks, taskId])]);
30645
+ db.run(`UPDATE tasks SET blocks = ?, updated_at = ? WHERE id = ?`, [newBlocks, Date.now(), bid]);
30646
+ }
30647
+ const updated = updateTask(taskId, { blocked_by: newBlockedBy, status: "blocked" });
30648
+ logEvent({
30649
+ task_id: taskId,
30650
+ agent_id: agentId,
30651
+ event_type: "blocked",
30652
+ payload: { blocked_by: blockerIds }
30653
+ });
30654
+ return updated;
30559
30655
  });
30560
- return { task: updated };
30656
+ return { task: doBlock() };
30561
30657
  }
30562
- function detectCycle(taskId, candidateBlocker, visited = new Set) {
30658
+ function detectCycleFromMap(taskId, candidateBlocker, taskMap, visited = new Set) {
30563
30659
  if (candidateBlocker === taskId)
30564
30660
  return true;
30565
30661
  if (visited.has(candidateBlocker))
30566
30662
  return false;
30567
30663
  visited.add(candidateBlocker);
30568
- const blocker = getTask(candidateBlocker);
30664
+ const blocker = taskMap.get(candidateBlocker);
30569
30665
  if (!blocker)
30570
30666
  return false;
30571
- return blocker.blocked_by.some((id) => detectCycle(taskId, id, visited));
30667
+ return blocker.blocked_by.some((id) => detectCycleFromMap(taskId, id, taskMap, visited));
30572
30668
  }
30573
30669
  function unblockTask(taskId, fromId, agentId) {
30670
+ const db = stmts2().db;
30574
30671
  const task = getTask(taskId);
30575
30672
  if (!task)
30576
30673
  return { task: null, error: "Task not found" };
@@ -30578,9 +30675,10 @@ function unblockTask(taskId, fromId, agentId) {
30578
30675
  return { task: null, error: `${taskId} is not blocked by ${fromId}` };
30579
30676
  }
30580
30677
  const remaining = task.blocked_by.filter((id) => id !== fromId);
30581
- const blocker = getTask(fromId);
30582
- if (blocker) {
30583
- updateTask(fromId, { blocks: blocker.blocks.filter((id) => id !== taskId) });
30678
+ const blockerRow = db.query("SELECT blocks FROM tasks WHERE id = ?").get(fromId);
30679
+ if (blockerRow) {
30680
+ const blocks = JSON.parse(blockerRow.blocks).filter((id) => id !== taskId);
30681
+ db.run(`UPDATE tasks SET blocks = ?, updated_at = ? WHERE id = ?`, [JSON.stringify(blocks), Date.now(), fromId]);
30584
30682
  }
30585
30683
  const newStatus = remaining.length === 0 ? "ready" : "blocked";
30586
30684
  const updated = updateTask(taskId, { blocked_by: remaining, status: newStatus });
@@ -30605,18 +30703,27 @@ function rejectTask(taskId, reason, agentId) {
30605
30703
  if (task.status !== "review") {
30606
30704
  return { task: null, error: "Task must be in review status to reject" };
30607
30705
  }
30608
- updateTask(taskId, { status: "in_progress" });
30609
- addImplementationNote(taskId, `REVIEW REJECTED: ${reason}`, agentId ?? "human");
30706
+ const agent = agentId ?? "human";
30707
+ const entry = {
30708
+ timestamp: Date.now(),
30709
+ agent,
30710
+ note: `REVIEW REJECTED: ${reason}`
30711
+ };
30712
+ const updated = updateTask(taskId, {
30713
+ status: "in_progress",
30714
+ implementation_notes: [...task.implementation_notes, entry]
30715
+ });
30716
+ logEvent({ task_id: taskId, agent_id: agentId, event_type: "note_added", payload: { note: entry.note } });
30610
30717
  logEvent({
30611
30718
  task_id: taskId,
30612
30719
  agent_id: agentId,
30613
30720
  event_type: "rejected",
30614
30721
  payload: { reason }
30615
30722
  });
30616
- return { task: getTask(taskId) };
30723
+ return { task: updated };
30617
30724
  }
30618
30725
  function unclaimTask(taskId, agentId, reason) {
30619
- const db = getDb();
30726
+ const s = stmts2();
30620
30727
  const task = getTask(taskId);
30621
30728
  if (!task)
30622
30729
  return { task: null, error: "Task not found" };
@@ -30624,89 +30731,109 @@ function unclaimTask(taskId, agentId, reason) {
30624
30731
  return { task: null, error: `Task is not assigned to ${agentId}` };
30625
30732
  }
30626
30733
  const backStatus = task.blocked_by.length > 0 ? "blocked" : "ready";
30627
- updateTask(taskId, { assigned_to: null, status: backStatus, started_at: null });
30734
+ let updated;
30628
30735
  if (reason) {
30629
- addImplementationNote(taskId, `UNCLAIMED by ${agentId}: ${reason}`, agentId);
30736
+ const entry = {
30737
+ timestamp: Date.now(),
30738
+ agent: agentId,
30739
+ note: `UNCLAIMED by ${agentId}: ${reason}`
30740
+ };
30741
+ updated = updateTask(taskId, {
30742
+ assigned_to: null,
30743
+ status: backStatus,
30744
+ started_at: null,
30745
+ implementation_notes: [...task.implementation_notes, entry]
30746
+ });
30747
+ logEvent({ task_id: taskId, agent_id: agentId, event_type: "note_added", payload: { note: entry.note } });
30748
+ } else {
30749
+ updated = updateTask(taskId, { assigned_to: null, status: backStatus, started_at: null });
30630
30750
  }
30631
- db.run(`UPDATE agents SET current_task = NULL WHERE id = ?`, [agentId]);
30751
+ s.releaseAgent.run(agentId);
30632
30752
  logEvent({
30633
30753
  task_id: taskId,
30634
30754
  agent_id: agentId,
30635
30755
  event_type: "unclaimed",
30636
30756
  payload: { reason: reason ?? "" }
30637
30757
  });
30638
- return { task: getTask(taskId) };
30758
+ return { task: updated };
30639
30759
  }
30640
30760
  function heartbeat(agentId, taskId) {
30641
- const db = getDb();
30642
- db.run(`INSERT INTO agents (id, first_seen, last_seen, current_task)
30643
- VALUES (?, ?, ?, ?)
30644
- ON CONFLICT(id) DO UPDATE SET last_seen = excluded.last_seen`, [agentId, Date.now(), Date.now(), taskId ?? null]);
30761
+ const now = Date.now();
30762
+ stmts2().heartbeat.run(agentId, now, now, taskId ?? null);
30645
30763
  }
30646
30764
  function listAgents() {
30647
- const db = getDb();
30765
+ const db = stmts2().db;
30648
30766
  return db.query("SELECT * FROM agents ORDER BY last_seen DESC").all();
30649
30767
  }
30650
30768
  function getNextTask(agentId) {
30651
- const tasks = listTasks({ status: "ready" });
30769
+ const db = stmts2().db;
30652
30770
  if (agentId) {
30653
- const mine = tasks.find((t) => t.assigned_to === agentId);
30771
+ const mine = db.query(`SELECT * FROM tasks WHERE status = 'ready' AND assigned_to = ?
30772
+ ORDER BY ${PRIORITY_ORDER}, created_at ASC LIMIT 1`).get(agentId);
30654
30773
  if (mine)
30655
- return mine;
30774
+ return parseTask(mine);
30656
30775
  }
30657
- return tasks.find((t) => t.assigned_to === null) ?? tasks[0] ?? null;
30776
+ const unassigned = db.query(`SELECT * FROM tasks WHERE status = 'ready' AND assigned_to IS NULL
30777
+ ORDER BY ${PRIORITY_ORDER}, created_at ASC LIMIT 1`).get();
30778
+ if (unassigned)
30779
+ return parseTask(unassigned);
30780
+ const any = db.query(`SELECT * FROM tasks WHERE status = 'ready'
30781
+ ORDER BY ${PRIORITY_ORDER}, created_at ASC LIMIT 1`).get();
30782
+ return any ? parseTask(any) : null;
30658
30783
  }
30659
30784
  function getStats() {
30660
- const db = getDb();
30661
- const byStatus = db.query("SELECT status, COUNT(*) as count FROM tasks GROUP BY status").all();
30662
- const byPriority = db.query("SELECT priority, COUNT(*) as count FROM tasks GROUP BY priority").all();
30663
- const byType = db.query("SELECT type, COUNT(*) as count FROM tasks GROUP BY type").all();
30664
- const total = db.query("SELECT COUNT(*) as count FROM tasks").get().count;
30665
- return {
30666
- by_status: Object.fromEntries(byStatus.map((r2) => [r2.status, r2.count])),
30667
- by_priority: Object.fromEntries(byPriority.map((r2) => [r2.priority, r2.count])),
30668
- by_type: Object.fromEntries(byType.map((r2) => [r2.type, r2.count])),
30669
- total
30670
- };
30785
+ const db = stmts2().db;
30786
+ const rows = db.query("SELECT status, priority, type, COUNT(*) as count FROM tasks GROUP BY status, priority, type").all();
30787
+ const by_status = {};
30788
+ const by_priority = {};
30789
+ const by_type = {};
30790
+ let total = 0;
30791
+ for (const row of rows) {
30792
+ by_status[row.status] = (by_status[row.status] ?? 0) + row.count;
30793
+ by_priority[row.priority] = (by_priority[row.priority] ?? 0) + row.count;
30794
+ by_type[row.type] = (by_type[row.type] ?? 0) + row.count;
30795
+ total += row.count;
30796
+ }
30797
+ return { by_status, by_priority, by_type, total };
30671
30798
  }
30672
30799
  function deleteTask(taskId, agentId) {
30673
- const db = getDb();
30800
+ const s = stmts2();
30801
+ const db = s.db;
30674
30802
  const task = getTask(taskId);
30675
30803
  if (!task) {
30676
30804
  return { success: false, error: "Task not found" };
30677
30805
  }
30678
- const subtasks = listTasks({ parent_id: taskId });
30679
- if (subtasks.length > 0) {
30806
+ const subtaskCount = s.subtaskCount.get(taskId).count;
30807
+ if (subtaskCount > 0) {
30680
30808
  return {
30681
30809
  success: false,
30682
- error: `Cannot delete ${taskId} - it has ${subtasks.length} subtask(s). Delete subtasks first.`
30810
+ error: `Cannot delete ${taskId} - it has ${subtaskCount} subtask(s). Delete subtasks first.`
30683
30811
  };
30684
30812
  }
30685
- logEvent({
30686
- task_id: taskId,
30687
- agent_id: agentId,
30688
- event_type: "deleted",
30689
- payload: {}
30813
+ const doDelete = db.transaction(() => {
30814
+ logEvent({
30815
+ task_id: taskId,
30816
+ agent_id: agentId,
30817
+ event_type: "deleted",
30818
+ payload: {}
30819
+ });
30820
+ const blocksRows = db.query(`SELECT id, blocks FROM tasks WHERE blocks LIKE '%' || ? || '%' AND id != ?`).all(taskId, taskId);
30821
+ for (const row of blocksRows) {
30822
+ const blocks = JSON.parse(row.blocks).filter((id) => id !== taskId);
30823
+ db.run(`UPDATE tasks SET blocks = ?, updated_at = ? WHERE id = ?`, [JSON.stringify(blocks), Date.now(), row.id]);
30824
+ }
30825
+ const blockedByRows = db.query(`SELECT id, blocked_by FROM tasks WHERE blocked_by LIKE '%' || ? || '%' AND id != ?`).all(taskId, taskId);
30826
+ for (const row of blockedByRows) {
30827
+ const remaining = JSON.parse(row.blocked_by).filter((id) => id !== taskId);
30828
+ const newStatus = remaining.length === 0 ? "ready" : "blocked";
30829
+ db.run(`UPDATE tasks SET blocked_by = ?, status = ?, updated_at = ? WHERE id = ?`, [JSON.stringify(remaining), newStatus, Date.now(), row.id]);
30830
+ }
30831
+ db.run(`UPDATE tasks SET parent_id = NULL, updated_at = ? WHERE parent_id = ?`, [Date.now(), taskId]);
30832
+ db.run("DELETE FROM events WHERE task_id = ?", [taskId]);
30833
+ db.run("UPDATE agents SET current_task = NULL WHERE current_task = ?", [taskId]);
30834
+ db.run("DELETE FROM tasks WHERE id = ?", [taskId]);
30690
30835
  });
30691
- const allTasks = listTasks();
30692
- for (const t of allTasks) {
30693
- if (t.blocks.includes(taskId)) {
30694
- updateTask(t.id, { blocks: t.blocks.filter((id) => id !== taskId) });
30695
- }
30696
- if (t.blocked_by.includes(taskId)) {
30697
- const remaining = t.blocked_by.filter((id) => id !== taskId);
30698
- updateTask(t.id, {
30699
- blocked_by: remaining,
30700
- status: remaining.length === 0 ? "ready" : "blocked"
30701
- });
30702
- }
30703
- if (t.parent_id === taskId) {
30704
- updateTask(t.id, { parent_id: null });
30705
- }
30706
- }
30707
- db.run("DELETE FROM events WHERE task_id = ?", [taskId]);
30708
- db.run("UPDATE agents SET current_task = NULL WHERE current_task = ?", [taskId]);
30709
- db.run("DELETE FROM tasks WHERE id = ?", [taskId]);
30836
+ doDelete();
30710
30837
  return { success: true };
30711
30838
  }
30712
30839
 
@@ -41096,7 +41223,7 @@ var claimCommand = new Command("claim").description("Claim task(s) to work on th
41096
41223
  requireInitialized();
41097
41224
  const json = isJsonMode(opts.json);
41098
41225
  const agent = requireAgentId(opts.agent, "claim");
41099
- const allTaskIds = listTasks().map((t) => t.id);
41226
+ const allTaskIds = getAllTaskIds();
41100
41227
  const resolvedIds = resolveTaskIds(taskIds.map((id) => id.toUpperCase()), allTaskIds);
41101
41228
  if (resolvedIds.length === 0) {
41102
41229
  const patternUsed = taskIds.some((id) => isPattern(id));
@@ -41143,7 +41270,7 @@ var startCommand = new Command("start").description("Start working on task(s) (t
41143
41270
  requireInitialized();
41144
41271
  const json = isJsonMode(opts.json);
41145
41272
  const agent = requireAgentId(opts.agent, "start");
41146
- const allTaskIds = listTasks().map((t) => t.id);
41273
+ const allTaskIds = getAllTaskIds();
41147
41274
  const resolvedIds = resolveTaskIds(taskIds.map((id) => id.toUpperCase()), allTaskIds);
41148
41275
  if (resolvedIds.length === 0) {
41149
41276
  const patternUsed = taskIds.some((id) => isPattern(id));
@@ -41247,7 +41374,7 @@ var doneCommand = new Command("done").description("Mark task(s) as complete (all
41247
41374
  requireInitialized();
41248
41375
  const json = isJsonMode(opts.json);
41249
41376
  const agent = opts.agent ?? process.env.AITASKS_AGENT_ID;
41250
- const allTaskIds = listTasks().map((t) => t.id);
41377
+ const allTaskIds = getAllTaskIds();
41251
41378
  const resolvedIds = resolveTaskIds(taskIds.map((id) => id.toUpperCase()), allTaskIds);
41252
41379
  if (resolvedIds.length === 0) {
41253
41380
  const patternUsed = taskIds.some((id) => isPattern(id));
@@ -41343,7 +41470,7 @@ var reviewCommand = new Command("review").description("Request human review for
41343
41470
  requireInitialized();
41344
41471
  const json = isJsonMode(opts.json);
41345
41472
  const agent = requireAgentId(opts.agent, "review");
41346
- const allTaskIds = listTasks().map((t) => t.id);
41473
+ const allTaskIds = getAllTaskIds();
41347
41474
  const resolvedIds = resolveTaskIds(taskIds.map((id) => id.toUpperCase()), allTaskIds);
41348
41475
  if (resolvedIds.length === 0) {
41349
41476
  const patternUsed = taskIds.some((id) => isPattern(id));
@@ -42085,7 +42212,20 @@ var TreeBoardComponent = ({ getTasks }) => {
42085
42212
  const newStatus = statusMap[input];
42086
42213
  if (newStatus && selectedTask) {
42087
42214
  if (newStatus === "done") {
42088
- const { error } = completeTask(selectedTask.id);
42215
+ let { error, unchecked } = completeTask(selectedTask.id);
42216
+ if (error && unchecked && unchecked.length > 0) {
42217
+ const task = getTask(selectedTask.id);
42218
+ if (task) {
42219
+ for (let i = 0;i < task.acceptance_criteria.length; i++) {
42220
+ const alreadyChecked = task.test_results.some((r2) => r2.index === i);
42221
+ if (!alreadyChecked) {
42222
+ checkCriterion(selectedTask.id, i, "auto-verified (moved to Done on board)", "human");
42223
+ }
42224
+ }
42225
+ const retry = completeTask(selectedTask.id);
42226
+ error = retry.error;
42227
+ }
42228
+ }
42089
42229
  if (error) {
42090
42230
  setMoveError(error.split(`
42091
42231
  `)[0] ?? error);
@@ -42996,7 +43136,7 @@ var searchCommand = new Command("search").description("Full-text search across t
42996
43136
  }
42997
43137
  const query = queryParts.join(" ").toLowerCase();
42998
43138
  const searchTerms = query.split(/\s+/).filter(Boolean);
42999
- const allTasks = listTasks(opts.status ? { status: opts.status } : {});
43139
+ const allTasks = searchTasks(searchTerms, opts.status);
43000
43140
  const results = allTasks.filter((task) => matchesSearch(task, searchTerms));
43001
43141
  if (results.length === 0) {
43002
43142
  if (json)
@@ -43253,7 +43393,7 @@ var deleteCommand = new Command("delete").description("Delete task(s) - does not
43253
43393
  requireInitialized();
43254
43394
  const json = isJsonMode(opts.json);
43255
43395
  const agent = opts.agent || process.env.AITASKS_AGENT_ID;
43256
- const allTaskIds = listTasks().map((t) => t.id);
43396
+ const allTaskIds = getAllTaskIds();
43257
43397
  const resolvedIds = expandPatterns(taskIds.map((id) => id.toUpperCase()), allTaskIds);
43258
43398
  if (resolvedIds.length === 0) {
43259
43399
  exitError("No tasks match the provided IDs/patterns", json);
@@ -43374,4 +43514,4 @@ program2.parseAsync(process.argv).catch((err) => {
43374
43514
  process.exit(1);
43375
43515
  });
43376
43516
 
43377
- //# debugId=517F4D02BD4703B064756E2164756E21
43517
+ //# debugId=55B7B23CFF99871064756E2164756E21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aitasks",
3
- "version": "1.4.0",
3
+ "version": "1.4.2",
4
4
  "description": "CLI task management tool built for AI agents",
5
5
  "type": "module",
6
6
  "bin": {