@hasna/todos 0.11.72 → 0.11.73
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/README.md +1 -1
- package/dist/cli/commands/mcp-hooks-commands.d.ts.map +1 -1
- package/dist/cli/commands/project-commands.d.ts.map +1 -1
- package/dist/cli/commands/query-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/index.js +2027 -1706
- package/dist/cli-mcp-parity.d.ts.map +1 -1
- package/dist/contracts.js +6500 -6251
- package/dist/db/api-keys.d.ts +9 -0
- package/dist/db/api-keys.d.ts.map +1 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/task-crud.d.ts.map +1 -1
- package/dist/db/task-lifecycle.d.ts +1 -0
- package/dist/db/task-lifecycle.d.ts.map +1 -1
- package/dist/db/task-status.d.ts.map +1 -1
- package/dist/index.js +8929 -8433
- package/dist/lib/db-backup.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1838 -1638
- package/dist/mcp/token-utils.d.ts.map +1 -1
- package/dist/mcp.js +6 -8
- package/dist/registry.js +6500 -6251
- package/dist/release-provenance.json +3 -3
- package/dist/sdk/client.d.ts.map +1 -1
- package/dist/sdk/index.js +6 -4
- package/dist/server/index.js +1838 -1638
- package/dist/server/routes.d.ts +2 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/storage/postgres-sync.d.ts.map +1 -1
- package/dist/storage.js +3298 -3022
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -3890,6 +3890,11 @@ function ensureSchema(db) {
|
|
|
3890
3890
|
ensureColumn("tasks", "priority_score", "INTEGER");
|
|
3891
3891
|
ensureColumn("tasks", "priority_reason", "TEXT");
|
|
3892
3892
|
ensureColumn("tasks", "archived_at", "TEXT");
|
|
3893
|
+
ensureColumn("tasks", "runner_id", "TEXT");
|
|
3894
|
+
ensureColumn("tasks", "runner_started_at", "TEXT");
|
|
3895
|
+
ensureColumn("tasks", "runner_completed_at", "TEXT");
|
|
3896
|
+
ensureColumn("tasks", "current_step", "TEXT");
|
|
3897
|
+
ensureColumn("tasks", "total_steps", "INTEGER");
|
|
3893
3898
|
ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
|
|
3894
3899
|
ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
|
|
3895
3900
|
ensureColumn("agents", "reports_to", "TEXT");
|
|
@@ -3897,6 +3902,10 @@ function ensureSchema(db) {
|
|
|
3897
3902
|
ensureColumn("agents", "level", "TEXT");
|
|
3898
3903
|
ensureColumn("agents", "org_id", "TEXT");
|
|
3899
3904
|
ensureColumn("agents", "capabilities", "TEXT DEFAULT '[]'");
|
|
3905
|
+
ensureColumn("agents", "session_id", "TEXT");
|
|
3906
|
+
ensureColumn("agents", "working_dir", "TEXT");
|
|
3907
|
+
ensureColumn("agents", "active_project_id", "TEXT");
|
|
3908
|
+
ensureColumn("agents", "status", "TEXT NOT NULL DEFAULT 'active'");
|
|
3900
3909
|
ensureColumn("projects", "org_id", "TEXT");
|
|
3901
3910
|
ensureColumn("plans", "slug", "TEXT");
|
|
3902
3911
|
ensureColumn("plans", "task_list_id", "TEXT");
|
|
@@ -4819,34 +4828,40 @@ function ensureDir(filePath) {
|
|
|
4819
4828
|
mkdirSync(dir, { recursive: true });
|
|
4820
4829
|
}
|
|
4821
4830
|
}
|
|
4831
|
+
function openDatabase(path) {
|
|
4832
|
+
ensureDir(path);
|
|
4833
|
+
const db = new Database(path);
|
|
4834
|
+
db.run("PRAGMA journal_mode = WAL");
|
|
4835
|
+
db.run("PRAGMA busy_timeout = 5000");
|
|
4836
|
+
db.run("PRAGMA foreign_keys = ON");
|
|
4837
|
+
runMigrations(db);
|
|
4838
|
+
backfillTaskTags(db);
|
|
4839
|
+
backfillMachineId(db);
|
|
4840
|
+
return db;
|
|
4841
|
+
}
|
|
4822
4842
|
function getDatabase(dbPath) {
|
|
4823
4843
|
const path = dbPath || getDbPath();
|
|
4824
4844
|
if (_db && _dbPath === path)
|
|
4825
4845
|
return _db;
|
|
4826
|
-
|
|
4827
|
-
_db.close();
|
|
4828
|
-
_db = null;
|
|
4829
|
-
_dbPath = null;
|
|
4830
|
-
}
|
|
4831
|
-
ensureDir(path);
|
|
4832
|
-
_db = new Database(path);
|
|
4846
|
+
_db = openDatabase(path);
|
|
4833
4847
|
_dbPath = path;
|
|
4834
|
-
_db.run("PRAGMA journal_mode = WAL");
|
|
4835
|
-
_db.run("PRAGMA busy_timeout = 5000");
|
|
4836
|
-
_db.run("PRAGMA foreign_keys = ON");
|
|
4837
|
-
runMigrations(_db);
|
|
4838
|
-
backfillTaskTags(_db);
|
|
4839
|
-
backfillMachineId(_db);
|
|
4840
4848
|
return _db;
|
|
4841
4849
|
}
|
|
4842
4850
|
function closeDatabase() {
|
|
4843
4851
|
if (_db) {
|
|
4844
|
-
|
|
4852
|
+
try {
|
|
4853
|
+
_db.close();
|
|
4854
|
+
} catch {}
|
|
4845
4855
|
_db = null;
|
|
4846
4856
|
_dbPath = null;
|
|
4847
4857
|
}
|
|
4848
4858
|
}
|
|
4849
4859
|
function resetDatabase() {
|
|
4860
|
+
if (_db) {
|
|
4861
|
+
try {
|
|
4862
|
+
_db.close();
|
|
4863
|
+
} catch {}
|
|
4864
|
+
}
|
|
4850
4865
|
_db = null;
|
|
4851
4866
|
_dbPath = null;
|
|
4852
4867
|
}
|
|
@@ -7930,1767 +7945,1858 @@ var init_checklists = __esm(() => {
|
|
|
7930
7945
|
init_database();
|
|
7931
7946
|
});
|
|
7932
7947
|
|
|
7933
|
-
// src/
|
|
7934
|
-
function
|
|
7948
|
+
// src/lib/recurrence.ts
|
|
7949
|
+
function parseRecurrenceRule(rule) {
|
|
7950
|
+
const normalized = rule.trim().toLowerCase();
|
|
7951
|
+
if (normalized === "every weekday" || normalized === "every weekdays") {
|
|
7952
|
+
return { type: "specific_days", days: [1, 2, 3, 4, 5] };
|
|
7953
|
+
}
|
|
7954
|
+
if (normalized === "every day" || normalized === "daily") {
|
|
7955
|
+
return { type: "interval", interval: 1, unit: "day" };
|
|
7956
|
+
}
|
|
7957
|
+
if (normalized === "every week" || normalized === "weekly") {
|
|
7958
|
+
return { type: "interval", interval: 1, unit: "week" };
|
|
7959
|
+
}
|
|
7960
|
+
if (normalized === "every month" || normalized === "monthly") {
|
|
7961
|
+
return { type: "interval", interval: 1, unit: "month" };
|
|
7962
|
+
}
|
|
7963
|
+
const intervalMatch = normalized.match(/^every\s+(\d+)\s+(day|week|month)s?$/);
|
|
7964
|
+
if (intervalMatch) {
|
|
7965
|
+
return {
|
|
7966
|
+
type: "interval",
|
|
7967
|
+
interval: parseInt(intervalMatch[1], 10),
|
|
7968
|
+
unit: intervalMatch[2]
|
|
7969
|
+
};
|
|
7970
|
+
}
|
|
7971
|
+
const daysMatch = normalized.match(/^every\s+(.+)$/);
|
|
7972
|
+
if (daysMatch) {
|
|
7973
|
+
const dayParts = daysMatch[1].split(/[,\s]+/).map((d) => d.trim()).filter(Boolean);
|
|
7974
|
+
const days = [];
|
|
7975
|
+
for (const part of dayParts) {
|
|
7976
|
+
const dayNum = DAY_NAMES[part];
|
|
7977
|
+
if (dayNum !== undefined) {
|
|
7978
|
+
days.push(dayNum);
|
|
7979
|
+
}
|
|
7980
|
+
}
|
|
7981
|
+
if (days.length > 0) {
|
|
7982
|
+
return { type: "specific_days", days: days.sort((a, b) => a - b) };
|
|
7983
|
+
}
|
|
7984
|
+
}
|
|
7985
|
+
throw new Error(`Invalid recurrence rule: "${rule}". Supported formats: "every day", "every weekday", "every week", "every 2 weeks", "every month", "every N days/weeks/months", "every monday", "every mon,wed,fri"`);
|
|
7986
|
+
}
|
|
7987
|
+
function isValidRecurrenceRule(rule) {
|
|
7988
|
+
try {
|
|
7989
|
+
parseRecurrenceRule(rule);
|
|
7990
|
+
return true;
|
|
7991
|
+
} catch {
|
|
7992
|
+
return false;
|
|
7993
|
+
}
|
|
7994
|
+
}
|
|
7995
|
+
function nextOccurrence(rule, from) {
|
|
7996
|
+
const parsed = parseRecurrenceRule(rule);
|
|
7997
|
+
const base = from || new Date;
|
|
7998
|
+
if (parsed.type === "interval") {
|
|
7999
|
+
const next = new Date(base);
|
|
8000
|
+
if (parsed.unit === "day") {
|
|
8001
|
+
next.setDate(next.getDate() + parsed.interval);
|
|
8002
|
+
} else if (parsed.unit === "week") {
|
|
8003
|
+
next.setDate(next.getDate() + parsed.interval * 7);
|
|
8004
|
+
} else if (parsed.unit === "month") {
|
|
8005
|
+
next.setMonth(next.getMonth() + parsed.interval);
|
|
8006
|
+
}
|
|
8007
|
+
return next.toISOString();
|
|
8008
|
+
}
|
|
8009
|
+
if (parsed.type === "specific_days") {
|
|
8010
|
+
const currentDay = base.getDay();
|
|
8011
|
+
const days = parsed.days;
|
|
8012
|
+
let daysToAdd = Infinity;
|
|
8013
|
+
for (const day of days) {
|
|
8014
|
+
let diff = day - currentDay;
|
|
8015
|
+
if (diff <= 0)
|
|
8016
|
+
diff += 7;
|
|
8017
|
+
if (diff < daysToAdd)
|
|
8018
|
+
daysToAdd = diff;
|
|
8019
|
+
}
|
|
8020
|
+
const next = new Date(base);
|
|
8021
|
+
next.setDate(next.getDate() + daysToAdd);
|
|
8022
|
+
return next.toISOString();
|
|
8023
|
+
}
|
|
8024
|
+
throw new Error(`Cannot calculate next occurrence for rule: "${rule}"`);
|
|
8025
|
+
}
|
|
8026
|
+
var DAY_NAMES;
|
|
8027
|
+
var init_recurrence = __esm(() => {
|
|
8028
|
+
DAY_NAMES = {
|
|
8029
|
+
sunday: 0,
|
|
8030
|
+
sun: 0,
|
|
8031
|
+
monday: 1,
|
|
8032
|
+
mon: 1,
|
|
8033
|
+
tuesday: 2,
|
|
8034
|
+
tue: 2,
|
|
8035
|
+
wednesday: 3,
|
|
8036
|
+
wed: 3,
|
|
8037
|
+
thursday: 4,
|
|
8038
|
+
thu: 4,
|
|
8039
|
+
friday: 5,
|
|
8040
|
+
fri: 5,
|
|
8041
|
+
saturday: 6,
|
|
8042
|
+
sat: 6
|
|
8043
|
+
};
|
|
8044
|
+
});
|
|
8045
|
+
|
|
8046
|
+
// src/db/templates.ts
|
|
8047
|
+
var exports_templates = {};
|
|
8048
|
+
__export(exports_templates, {
|
|
8049
|
+
updateTemplate: () => updateTemplate,
|
|
8050
|
+
tasksFromTemplate: () => tasksFromTemplate,
|
|
8051
|
+
taskFromTemplate: () => taskFromTemplate,
|
|
8052
|
+
resolveVariables: () => resolveVariables,
|
|
8053
|
+
previewTemplate: () => previewTemplate,
|
|
8054
|
+
listTemplates: () => listTemplates,
|
|
8055
|
+
listTemplateVersions: () => listTemplateVersions,
|
|
8056
|
+
importTemplate: () => importTemplate,
|
|
8057
|
+
getTemplateWithTasks: () => getTemplateWithTasks,
|
|
8058
|
+
getTemplateVersion: () => getTemplateVersion,
|
|
8059
|
+
getTemplateTasks: () => getTemplateTasks,
|
|
8060
|
+
getTemplate: () => getTemplate,
|
|
8061
|
+
exportTemplate: () => exportTemplate,
|
|
8062
|
+
evaluateCondition: () => evaluateCondition,
|
|
8063
|
+
deleteTemplate: () => deleteTemplate,
|
|
8064
|
+
createTemplate: () => createTemplate,
|
|
8065
|
+
addTemplateTasks: () => addTemplateTasks
|
|
8066
|
+
});
|
|
8067
|
+
function rowToTemplate(row) {
|
|
7935
8068
|
return {
|
|
7936
8069
|
...row,
|
|
7937
8070
|
tags: JSON.parse(row.tags || "[]"),
|
|
8071
|
+
variables: JSON.parse(row.variables || "[]"),
|
|
7938
8072
|
metadata: JSON.parse(row.metadata || "{}"),
|
|
7939
|
-
|
|
7940
|
-
|
|
7941
|
-
requires_approval: !!row.requires_approval
|
|
8073
|
+
priority: row.priority || "medium",
|
|
8074
|
+
version: row.version ?? 1
|
|
7942
8075
|
};
|
|
7943
8076
|
}
|
|
7944
|
-
function
|
|
7945
|
-
|
|
7946
|
-
|
|
7947
|
-
|
|
7948
|
-
|
|
7949
|
-
|
|
7950
|
-
|
|
7951
|
-
|
|
7952
|
-
|
|
7953
|
-
|
|
7954
|
-
db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
|
|
7955
|
-
insertTaskTags(taskId, tags, db);
|
|
8077
|
+
function rowToTemplateTask(row) {
|
|
8078
|
+
return {
|
|
8079
|
+
...row,
|
|
8080
|
+
tags: JSON.parse(row.tags || "[]"),
|
|
8081
|
+
depends_on_positions: JSON.parse(row.depends_on_positions || "[]"),
|
|
8082
|
+
metadata: JSON.parse(row.metadata || "{}"),
|
|
8083
|
+
priority: row.priority || "medium",
|
|
8084
|
+
condition: row.condition ?? null,
|
|
8085
|
+
include_template_id: row.include_template_id ?? null
|
|
8086
|
+
};
|
|
7956
8087
|
}
|
|
7957
|
-
function
|
|
7958
|
-
|
|
7959
|
-
return;
|
|
7960
|
-
for (const [key, value] of Object.entries(metadata)) {
|
|
7961
|
-
if (!/^[A-Za-z0-9_.-]+$/.test(key)) {
|
|
7962
|
-
throw new Error(`Invalid metadata filter key: ${key}`);
|
|
7963
|
-
}
|
|
7964
|
-
conditions.push(`json_extract(metadata, '$."${key}"') = ?`);
|
|
7965
|
-
params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
|
|
7966
|
-
}
|
|
8088
|
+
function resolveTemplateId(id, d) {
|
|
8089
|
+
return resolvePartialId(d, "task_templates", id);
|
|
7967
8090
|
}
|
|
7968
|
-
function
|
|
8091
|
+
function createTemplate(input, db) {
|
|
7969
8092
|
const d = db || getDatabase();
|
|
7970
|
-
const
|
|
7971
|
-
const tags = input.tags || [];
|
|
8093
|
+
const id = uuid();
|
|
7972
8094
|
const machineId = currentStorageMachineId(d);
|
|
7973
|
-
|
|
7974
|
-
|
|
7975
|
-
|
|
7976
|
-
|
|
7977
|
-
|
|
7978
|
-
|
|
7979
|
-
|
|
7980
|
-
|
|
7981
|
-
|
|
7982
|
-
|
|
7983
|
-
|
|
7984
|
-
|
|
7985
|
-
|
|
7986
|
-
|
|
7987
|
-
|
|
7988
|
-
|
|
7989
|
-
|
|
7990
|
-
input.priority || "medium",
|
|
7991
|
-
input.agent_id || null,
|
|
7992
|
-
input.assigned_to || null,
|
|
7993
|
-
input.session_id || null,
|
|
7994
|
-
input.working_dir || null,
|
|
7995
|
-
JSON.stringify(tags),
|
|
7996
|
-
JSON.stringify(input.metadata || {}),
|
|
7997
|
-
timestamp,
|
|
7998
|
-
timestamp,
|
|
7999
|
-
input.due_at || null,
|
|
8000
|
-
input.estimated_minutes || null,
|
|
8001
|
-
input.sla_minutes ?? null,
|
|
8002
|
-
input.confidence ?? null,
|
|
8003
|
-
input.retry_count ?? 0,
|
|
8004
|
-
input.max_retries ?? 3,
|
|
8005
|
-
input.retry_after ?? null,
|
|
8006
|
-
input.requires_approval ? 1 : 0,
|
|
8007
|
-
null,
|
|
8008
|
-
null,
|
|
8009
|
-
input.recurrence_rule || null,
|
|
8010
|
-
input.recurrence_parent_id || null,
|
|
8011
|
-
input.spawns_template_id || null,
|
|
8012
|
-
input.reason || null,
|
|
8013
|
-
input.spawned_from_session || null,
|
|
8014
|
-
assignedBy || null,
|
|
8015
|
-
assignedFromProject || null,
|
|
8016
|
-
input.task_type || null,
|
|
8017
|
-
machineId
|
|
8018
|
-
]);
|
|
8019
|
-
break;
|
|
8020
|
-
} catch (e) {
|
|
8021
|
-
if (attempt < 2 && e?.message?.includes("UNIQUE constraint failed: tasks.id")) {
|
|
8022
|
-
id = uuid();
|
|
8023
|
-
continue;
|
|
8024
|
-
}
|
|
8025
|
-
throw e;
|
|
8026
|
-
}
|
|
8027
|
-
}
|
|
8028
|
-
if (tags.length > 0) {
|
|
8029
|
-
insertTaskTags(id, tags, d);
|
|
8095
|
+
d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, variables, project_id, plan_id, metadata, created_at, machine_id)
|
|
8096
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
8097
|
+
id,
|
|
8098
|
+
input.name,
|
|
8099
|
+
input.title_pattern,
|
|
8100
|
+
input.description || null,
|
|
8101
|
+
input.priority || "medium",
|
|
8102
|
+
JSON.stringify(input.tags || []),
|
|
8103
|
+
JSON.stringify(input.variables || []),
|
|
8104
|
+
input.project_id || null,
|
|
8105
|
+
input.plan_id || null,
|
|
8106
|
+
JSON.stringify(input.metadata || {}),
|
|
8107
|
+
now(),
|
|
8108
|
+
machineId
|
|
8109
|
+
]);
|
|
8110
|
+
if (input.tasks && input.tasks.length > 0) {
|
|
8111
|
+
addTemplateTasks(id, input.tasks, d);
|
|
8030
8112
|
}
|
|
8031
|
-
|
|
8032
|
-
const payload = taskEventData(task);
|
|
8033
|
-
const databasePath = databasePathFromDatabase(d);
|
|
8034
|
-
dispatchWebhook2("task.created", payload, d).catch(() => {});
|
|
8035
|
-
emitLocalEventHooksQuiet({ type: "task.created", payload, databasePath });
|
|
8036
|
-
emitSharedTaskEventQuiet({ type: "task.created", task, databasePath });
|
|
8037
|
-
return task;
|
|
8113
|
+
return getTemplate(id, d);
|
|
8038
8114
|
}
|
|
8039
|
-
function
|
|
8115
|
+
function getTemplate(id, db) {
|
|
8040
8116
|
const d = db || getDatabase();
|
|
8041
|
-
const
|
|
8042
|
-
if (!
|
|
8117
|
+
const resolved = resolveTemplateId(id, d);
|
|
8118
|
+
if (!resolved)
|
|
8043
8119
|
return null;
|
|
8044
|
-
|
|
8120
|
+
const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(resolved);
|
|
8121
|
+
return row ? rowToTemplate(row) : null;
|
|
8045
8122
|
}
|
|
8046
|
-
function
|
|
8123
|
+
function listTemplates(db) {
|
|
8047
8124
|
const d = db || getDatabase();
|
|
8048
|
-
|
|
8049
|
-
if (!task)
|
|
8050
|
-
return null;
|
|
8051
|
-
const subtaskRows = d.query("SELECT * FROM tasks WHERE parent_id = ? ORDER BY created_at").all(id);
|
|
8052
|
-
const subtasks = subtaskRows.map(rowToTask);
|
|
8053
|
-
const depRows = d.query(`SELECT t.* FROM tasks t
|
|
8054
|
-
JOIN task_dependencies td ON td.depends_on = t.id
|
|
8055
|
-
WHERE td.task_id = ?`).all(id);
|
|
8056
|
-
const dependencies = depRows.map(rowToTask);
|
|
8057
|
-
const blockedByRows = d.query(`SELECT t.* FROM tasks t
|
|
8058
|
-
JOIN task_dependencies td ON td.task_id = t.id
|
|
8059
|
-
WHERE td.depends_on = ?`).all(id);
|
|
8060
|
-
const blocked_by = blockedByRows.map(rowToTask);
|
|
8061
|
-
const comments = d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(id);
|
|
8062
|
-
const parent = task.parent_id ? getTask(task.parent_id, d) : null;
|
|
8063
|
-
const checklist = getChecklist(id, d);
|
|
8064
|
-
return {
|
|
8065
|
-
...task,
|
|
8066
|
-
subtasks,
|
|
8067
|
-
dependencies,
|
|
8068
|
-
blocked_by,
|
|
8069
|
-
comments,
|
|
8070
|
-
parent,
|
|
8071
|
-
checklist
|
|
8072
|
-
};
|
|
8125
|
+
return d.query("SELECT * FROM task_templates ORDER BY name").all().map(rowToTemplate);
|
|
8073
8126
|
}
|
|
8074
|
-
function
|
|
8127
|
+
function deleteTemplate(id, db) {
|
|
8075
8128
|
const d = db || getDatabase();
|
|
8076
|
-
const
|
|
8077
|
-
|
|
8078
|
-
|
|
8079
|
-
const
|
|
8080
|
-
if (
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
}
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
|
|
8092
|
-
|
|
8093
|
-
|
|
8094
|
-
|
|
8095
|
-
|
|
8096
|
-
if (
|
|
8097
|
-
|
|
8098
|
-
|
|
8099
|
-
|
|
8100
|
-
|
|
8101
|
-
|
|
8102
|
-
|
|
8103
|
-
|
|
8104
|
-
|
|
8105
|
-
|
|
8106
|
-
|
|
8107
|
-
|
|
8108
|
-
|
|
8109
|
-
|
|
8110
|
-
conditions.push("priority = ?");
|
|
8111
|
-
params.push(filter.priority);
|
|
8112
|
-
}
|
|
8113
|
-
}
|
|
8114
|
-
if (filter.assigned_to) {
|
|
8115
|
-
conditions.push("assigned_to = ?");
|
|
8116
|
-
params.push(filter.assigned_to);
|
|
8117
|
-
}
|
|
8118
|
-
if (filter.agent_id) {
|
|
8119
|
-
conditions.push("agent_id = ?");
|
|
8120
|
-
params.push(filter.agent_id);
|
|
8129
|
+
const resolved = resolveTemplateId(id, d);
|
|
8130
|
+
if (!resolved)
|
|
8131
|
+
return false;
|
|
8132
|
+
const template = getTemplate(resolved, d);
|
|
8133
|
+
if (!template)
|
|
8134
|
+
return false;
|
|
8135
|
+
recordStorageTombstone({
|
|
8136
|
+
object_type: "templates",
|
|
8137
|
+
object_id: resolved,
|
|
8138
|
+
payload: template,
|
|
8139
|
+
version: template.version
|
|
8140
|
+
}, d);
|
|
8141
|
+
return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
|
|
8142
|
+
}
|
|
8143
|
+
function updateTemplate(id, updates, db) {
|
|
8144
|
+
const d = db || getDatabase();
|
|
8145
|
+
const resolved = resolveTemplateId(id, d);
|
|
8146
|
+
if (!resolved)
|
|
8147
|
+
return null;
|
|
8148
|
+
const current = getTemplateWithTasks(resolved, d);
|
|
8149
|
+
if (current) {
|
|
8150
|
+
const snapshot = JSON.stringify({
|
|
8151
|
+
name: current.name,
|
|
8152
|
+
title_pattern: current.title_pattern,
|
|
8153
|
+
description: current.description,
|
|
8154
|
+
priority: current.priority,
|
|
8155
|
+
tags: current.tags,
|
|
8156
|
+
variables: current.variables,
|
|
8157
|
+
project_id: current.project_id,
|
|
8158
|
+
plan_id: current.plan_id,
|
|
8159
|
+
metadata: current.metadata,
|
|
8160
|
+
tasks: current.tasks
|
|
8161
|
+
});
|
|
8162
|
+
d.run(`INSERT INTO template_versions (id, template_id, version, snapshot, created_at) VALUES (?, ?, ?, ?, ?)`, [uuid(), resolved, current.version, snapshot, now()]);
|
|
8121
8163
|
}
|
|
8122
|
-
|
|
8123
|
-
|
|
8124
|
-
|
|
8164
|
+
const sets = ["version = version + 1"];
|
|
8165
|
+
const values = [];
|
|
8166
|
+
if (updates.name !== undefined) {
|
|
8167
|
+
sets.push("name = ?");
|
|
8168
|
+
values.push(updates.name);
|
|
8125
8169
|
}
|
|
8126
|
-
if (
|
|
8127
|
-
|
|
8128
|
-
|
|
8129
|
-
params.push(...filter.tags);
|
|
8170
|
+
if (updates.title_pattern !== undefined) {
|
|
8171
|
+
sets.push("title_pattern = ?");
|
|
8172
|
+
values.push(updates.title_pattern);
|
|
8130
8173
|
}
|
|
8131
|
-
if (
|
|
8132
|
-
|
|
8133
|
-
|
|
8174
|
+
if (updates.description !== undefined) {
|
|
8175
|
+
sets.push("description = ?");
|
|
8176
|
+
values.push(updates.description);
|
|
8134
8177
|
}
|
|
8135
|
-
if (
|
|
8136
|
-
|
|
8137
|
-
|
|
8178
|
+
if (updates.priority !== undefined) {
|
|
8179
|
+
sets.push("priority = ?");
|
|
8180
|
+
values.push(updates.priority);
|
|
8138
8181
|
}
|
|
8139
|
-
if (
|
|
8140
|
-
|
|
8141
|
-
|
|
8142
|
-
conditions.push("recurrence_rule IS NULL");
|
|
8182
|
+
if (updates.tags !== undefined) {
|
|
8183
|
+
sets.push("tags = ?");
|
|
8184
|
+
values.push(JSON.stringify(updates.tags));
|
|
8143
8185
|
}
|
|
8144
|
-
if (
|
|
8145
|
-
|
|
8146
|
-
|
|
8147
|
-
params.push(...filter.task_type);
|
|
8148
|
-
} else {
|
|
8149
|
-
conditions.push("task_type = ?");
|
|
8150
|
-
params.push(filter.task_type);
|
|
8151
|
-
}
|
|
8186
|
+
if (updates.variables !== undefined) {
|
|
8187
|
+
sets.push("variables = ?");
|
|
8188
|
+
values.push(JSON.stringify(updates.variables));
|
|
8152
8189
|
}
|
|
8153
|
-
|
|
8154
|
-
|
|
8155
|
-
|
|
8156
|
-
try {
|
|
8157
|
-
const decoded = JSON.parse(Buffer.from(filter.cursor, "base64").toString("utf8"));
|
|
8158
|
-
conditions.push(`(${PRIORITY_RANK} > ? OR (${PRIORITY_RANK} = ? AND created_at < ?) OR (${PRIORITY_RANK} = ? AND created_at = ? AND id > ?))`);
|
|
8159
|
-
params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
|
|
8160
|
-
} catch {}
|
|
8190
|
+
if (updates.project_id !== undefined) {
|
|
8191
|
+
sets.push("project_id = ?");
|
|
8192
|
+
values.push(updates.project_id);
|
|
8161
8193
|
}
|
|
8162
|
-
if (
|
|
8163
|
-
|
|
8194
|
+
if (updates.plan_id !== undefined) {
|
|
8195
|
+
sets.push("plan_id = ?");
|
|
8196
|
+
values.push(updates.plan_id);
|
|
8164
8197
|
}
|
|
8165
|
-
|
|
8166
|
-
|
|
8167
|
-
|
|
8168
|
-
limitClause = " LIMIT ?";
|
|
8169
|
-
params.push(filter.limit);
|
|
8170
|
-
if (!filter.cursor && filter.offset) {
|
|
8171
|
-
limitClause += " OFFSET ?";
|
|
8172
|
-
params.push(filter.offset);
|
|
8173
|
-
}
|
|
8198
|
+
if (updates.metadata !== undefined) {
|
|
8199
|
+
sets.push("metadata = ?");
|
|
8200
|
+
values.push(JSON.stringify(updates.metadata));
|
|
8174
8201
|
}
|
|
8175
|
-
|
|
8176
|
-
|
|
8177
|
-
|
|
8178
|
-
function getTaskByFingerprint(fingerprint, db) {
|
|
8179
|
-
const tasks = listTasks({ metadata: { fingerprint }, limit: 1 }, db);
|
|
8180
|
-
return tasks[0] ?? null;
|
|
8202
|
+
values.push(resolved);
|
|
8203
|
+
d.run(`UPDATE task_templates SET ${sets.join(", ")} WHERE id = ?`, values);
|
|
8204
|
+
return getTemplate(resolved, d);
|
|
8181
8205
|
}
|
|
8182
|
-
function
|
|
8206
|
+
function taskFromTemplate(templateId, overrides = {}, db) {
|
|
8207
|
+
const t = getTemplate(templateId, db);
|
|
8208
|
+
if (!t)
|
|
8209
|
+
throw new Error(`Template not found: ${templateId}`);
|
|
8210
|
+
const cleanOverrides = Object.fromEntries(Object.entries(overrides).filter(([, v]) => v !== undefined));
|
|
8183
8211
|
return {
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
|
|
8212
|
+
title: cleanOverrides.title || t.title_pattern,
|
|
8213
|
+
description: cleanOverrides.description ?? t.description ?? undefined,
|
|
8214
|
+
priority: cleanOverrides.priority ?? t.priority,
|
|
8215
|
+
tags: cleanOverrides.tags ?? t.tags,
|
|
8216
|
+
project_id: cleanOverrides.project_id ?? t.project_id ?? undefined,
|
|
8217
|
+
plan_id: cleanOverrides.plan_id ?? t.plan_id ?? undefined,
|
|
8218
|
+
metadata: cleanOverrides.metadata ?? t.metadata,
|
|
8219
|
+
...cleanOverrides
|
|
8187
8220
|
};
|
|
8188
8221
|
}
|
|
8189
|
-
function
|
|
8222
|
+
function addTemplateTasks(templateId, tasks, db) {
|
|
8190
8223
|
const d = db || getDatabase();
|
|
8191
|
-
const
|
|
8192
|
-
if (!
|
|
8193
|
-
throw new Error(
|
|
8194
|
-
|
|
8195
|
-
const
|
|
8196
|
-
|
|
8197
|
-
const
|
|
8198
|
-
|
|
8224
|
+
const template = getTemplate(templateId, d);
|
|
8225
|
+
if (!template)
|
|
8226
|
+
throw new Error(`Template not found: ${templateId}`);
|
|
8227
|
+
d.run("DELETE FROM template_tasks WHERE template_id = ?", [templateId]);
|
|
8228
|
+
const results = [];
|
|
8229
|
+
for (let i = 0;i < tasks.length; i++) {
|
|
8230
|
+
const task = tasks[i];
|
|
8231
|
+
const id = uuid();
|
|
8232
|
+
d.run(`INSERT INTO template_tasks (id, template_id, position, title_pattern, description, priority, tags, task_type, condition, include_template_id, depends_on_positions, metadata, created_at)
|
|
8233
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
8234
|
+
id,
|
|
8235
|
+
templateId,
|
|
8236
|
+
i,
|
|
8237
|
+
task.title_pattern,
|
|
8238
|
+
task.description || null,
|
|
8239
|
+
task.priority || "medium",
|
|
8240
|
+
JSON.stringify(task.tags || []),
|
|
8241
|
+
task.task_type || null,
|
|
8242
|
+
task.condition || null,
|
|
8243
|
+
task.include_template_id || null,
|
|
8244
|
+
JSON.stringify(task.depends_on || []),
|
|
8245
|
+
JSON.stringify(task.metadata || {}),
|
|
8246
|
+
now()
|
|
8247
|
+
]);
|
|
8248
|
+
const row = d.query("SELECT * FROM template_tasks WHERE id = ?").get(id);
|
|
8249
|
+
if (row)
|
|
8250
|
+
results.push(rowToTemplateTask(row));
|
|
8199
8251
|
}
|
|
8200
|
-
|
|
8201
|
-
version: existing.version,
|
|
8202
|
-
title: input.title,
|
|
8203
|
-
description: input.description,
|
|
8204
|
-
status: input.status,
|
|
8205
|
-
priority: input.priority,
|
|
8206
|
-
project_id: input.project_id,
|
|
8207
|
-
assigned_to: input.assigned_to,
|
|
8208
|
-
working_dir: input.working_dir,
|
|
8209
|
-
plan_id: input.plan_id,
|
|
8210
|
-
task_list_id: input.task_list_id,
|
|
8211
|
-
tags: input.tags,
|
|
8212
|
-
metadata,
|
|
8213
|
-
due_at: input.due_at,
|
|
8214
|
-
estimated_minutes: input.estimated_minutes,
|
|
8215
|
-
sla_minutes: input.sla_minutes,
|
|
8216
|
-
confidence: input.confidence,
|
|
8217
|
-
retry_count: input.retry_count,
|
|
8218
|
-
max_retries: input.max_retries,
|
|
8219
|
-
retry_after: input.retry_after,
|
|
8220
|
-
requires_approval: input.requires_approval,
|
|
8221
|
-
recurrence_rule: input.recurrence_rule,
|
|
8222
|
-
task_type: input.task_type
|
|
8223
|
-
}, d);
|
|
8224
|
-
return { task, created: false };
|
|
8252
|
+
return results;
|
|
8225
8253
|
}
|
|
8226
|
-
function
|
|
8254
|
+
function getTemplateWithTasks(id, db) {
|
|
8227
8255
|
const d = db || getDatabase();
|
|
8228
|
-
const
|
|
8229
|
-
|
|
8230
|
-
|
|
8231
|
-
|
|
8232
|
-
|
|
8256
|
+
const template = getTemplate(id, d);
|
|
8257
|
+
if (!template)
|
|
8258
|
+
return null;
|
|
8259
|
+
const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(template.id);
|
|
8260
|
+
const tasks = rows.map(rowToTemplateTask);
|
|
8261
|
+
return { ...template, tasks };
|
|
8262
|
+
}
|
|
8263
|
+
function getTemplateTasks(templateId, db) {
|
|
8264
|
+
const d = db || getDatabase();
|
|
8265
|
+
const resolved = resolveTemplateId(templateId, d);
|
|
8266
|
+
if (!resolved)
|
|
8267
|
+
return [];
|
|
8268
|
+
const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(resolved);
|
|
8269
|
+
return rows.map(rowToTemplateTask);
|
|
8270
|
+
}
|
|
8271
|
+
function evaluateCondition(condition, variables) {
|
|
8272
|
+
if (!condition || condition.trim() === "")
|
|
8273
|
+
return true;
|
|
8274
|
+
const trimmed = condition.trim();
|
|
8275
|
+
const eqMatch = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
|
|
8276
|
+
if (eqMatch) {
|
|
8277
|
+
const varName = eqMatch[1];
|
|
8278
|
+
const expected = eqMatch[2].trim();
|
|
8279
|
+
return (variables[varName] ?? "") === expected;
|
|
8233
8280
|
}
|
|
8234
|
-
|
|
8235
|
-
|
|
8236
|
-
|
|
8281
|
+
const neqMatch = trimmed.match(/^\{([^}]+)\}\s*!=\s*(.+)$/);
|
|
8282
|
+
if (neqMatch) {
|
|
8283
|
+
const varName = neqMatch[1];
|
|
8284
|
+
const expected = neqMatch[2].trim();
|
|
8285
|
+
return (variables[varName] ?? "") !== expected;
|
|
8237
8286
|
}
|
|
8238
|
-
|
|
8239
|
-
|
|
8240
|
-
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
params.push(filter.parent_id);
|
|
8244
|
-
}
|
|
8287
|
+
const falsyMatch = trimmed.match(/^!\{([^}]+)\}$/);
|
|
8288
|
+
if (falsyMatch) {
|
|
8289
|
+
const varName = falsyMatch[1];
|
|
8290
|
+
const val = variables[varName];
|
|
8291
|
+
return !val || val === "" || val === "false";
|
|
8245
8292
|
}
|
|
8246
|
-
|
|
8247
|
-
|
|
8248
|
-
|
|
8249
|
-
|
|
8250
|
-
|
|
8251
|
-
conditions.push("status = ?");
|
|
8252
|
-
params.push(filter.status);
|
|
8253
|
-
}
|
|
8293
|
+
const truthyMatch = trimmed.match(/^\{([^}]+)\}$/);
|
|
8294
|
+
if (truthyMatch) {
|
|
8295
|
+
const varName = truthyMatch[1];
|
|
8296
|
+
const val = variables[varName];
|
|
8297
|
+
return !!val && val !== "" && val !== "false";
|
|
8254
8298
|
}
|
|
8255
|
-
|
|
8256
|
-
|
|
8257
|
-
|
|
8258
|
-
|
|
8259
|
-
|
|
8260
|
-
|
|
8261
|
-
|
|
8299
|
+
return true;
|
|
8300
|
+
}
|
|
8301
|
+
function exportTemplate(id, db) {
|
|
8302
|
+
const d = db || getDatabase();
|
|
8303
|
+
const template = getTemplateWithTasks(id, d);
|
|
8304
|
+
if (!template)
|
|
8305
|
+
throw new Error(`Template not found: ${id}`);
|
|
8306
|
+
return {
|
|
8307
|
+
name: template.name,
|
|
8308
|
+
title_pattern: template.title_pattern,
|
|
8309
|
+
description: template.description,
|
|
8310
|
+
priority: template.priority,
|
|
8311
|
+
tags: template.tags,
|
|
8312
|
+
variables: template.variables,
|
|
8313
|
+
project_id: template.project_id,
|
|
8314
|
+
plan_id: template.plan_id,
|
|
8315
|
+
metadata: template.metadata,
|
|
8316
|
+
tasks: template.tasks.map((t) => ({
|
|
8317
|
+
position: t.position,
|
|
8318
|
+
title_pattern: t.title_pattern,
|
|
8319
|
+
description: t.description,
|
|
8320
|
+
priority: t.priority,
|
|
8321
|
+
tags: t.tags,
|
|
8322
|
+
task_type: t.task_type,
|
|
8323
|
+
condition: t.condition,
|
|
8324
|
+
include_template_id: t.include_template_id,
|
|
8325
|
+
depends_on_positions: t.depends_on_positions,
|
|
8326
|
+
metadata: t.metadata
|
|
8327
|
+
}))
|
|
8328
|
+
};
|
|
8329
|
+
}
|
|
8330
|
+
function importTemplate(json, db) {
|
|
8331
|
+
const d = db || getDatabase();
|
|
8332
|
+
const taskInputs = (json.tasks || []).map((t) => ({
|
|
8333
|
+
title_pattern: t.title_pattern,
|
|
8334
|
+
description: t.description ?? undefined,
|
|
8335
|
+
priority: t.priority,
|
|
8336
|
+
tags: t.tags,
|
|
8337
|
+
task_type: t.task_type ?? undefined,
|
|
8338
|
+
condition: t.condition ?? undefined,
|
|
8339
|
+
include_template_id: t.include_template_id ?? undefined,
|
|
8340
|
+
depends_on: t.depends_on_positions,
|
|
8341
|
+
metadata: t.metadata
|
|
8342
|
+
}));
|
|
8343
|
+
return createTemplate({
|
|
8344
|
+
name: json.name,
|
|
8345
|
+
title_pattern: json.title_pattern,
|
|
8346
|
+
description: json.description ?? undefined,
|
|
8347
|
+
priority: json.priority,
|
|
8348
|
+
tags: json.tags,
|
|
8349
|
+
variables: json.variables,
|
|
8350
|
+
project_id: json.project_id ?? undefined,
|
|
8351
|
+
plan_id: json.plan_id ?? undefined,
|
|
8352
|
+
metadata: json.metadata,
|
|
8353
|
+
tasks: taskInputs
|
|
8354
|
+
}, d);
|
|
8355
|
+
}
|
|
8356
|
+
function getTemplateVersion(id, version, db) {
|
|
8357
|
+
const d = db || getDatabase();
|
|
8358
|
+
const resolved = resolveTemplateId(id, d);
|
|
8359
|
+
if (!resolved)
|
|
8360
|
+
return null;
|
|
8361
|
+
const row = d.query("SELECT * FROM template_versions WHERE template_id = ? AND version = ?").get(resolved, version);
|
|
8362
|
+
return row || null;
|
|
8363
|
+
}
|
|
8364
|
+
function listTemplateVersions(id, db) {
|
|
8365
|
+
const d = db || getDatabase();
|
|
8366
|
+
const resolved = resolveTemplateId(id, d);
|
|
8367
|
+
if (!resolved)
|
|
8368
|
+
return [];
|
|
8369
|
+
return d.query("SELECT * FROM template_versions WHERE template_id = ? ORDER BY version DESC").all(resolved);
|
|
8370
|
+
}
|
|
8371
|
+
function resolveVariables(templateVars, provided) {
|
|
8372
|
+
const merged = { ...provided };
|
|
8373
|
+
for (const v of templateVars) {
|
|
8374
|
+
if (merged[v.name] === undefined && v.default !== undefined) {
|
|
8375
|
+
merged[v.name] = v.default;
|
|
8262
8376
|
}
|
|
8263
8377
|
}
|
|
8264
|
-
|
|
8265
|
-
|
|
8266
|
-
|
|
8267
|
-
|
|
8268
|
-
|
|
8269
|
-
conditions.push("agent_id = ?");
|
|
8270
|
-
params.push(filter.agent_id);
|
|
8271
|
-
}
|
|
8272
|
-
if (filter.session_id) {
|
|
8273
|
-
conditions.push("session_id = ?");
|
|
8274
|
-
params.push(filter.session_id);
|
|
8275
|
-
}
|
|
8276
|
-
if (filter.tags && filter.tags.length > 0) {
|
|
8277
|
-
const placeholders = filter.tags.map(() => "?").join(",");
|
|
8278
|
-
conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
|
|
8279
|
-
params.push(...filter.tags);
|
|
8280
|
-
}
|
|
8281
|
-
if (filter.plan_id) {
|
|
8282
|
-
conditions.push("plan_id = ?");
|
|
8283
|
-
params.push(filter.plan_id);
|
|
8378
|
+
const missing = [];
|
|
8379
|
+
for (const v of templateVars) {
|
|
8380
|
+
if (v.required && merged[v.name] === undefined) {
|
|
8381
|
+
missing.push(v.name);
|
|
8382
|
+
}
|
|
8284
8383
|
}
|
|
8285
|
-
if (
|
|
8286
|
-
|
|
8287
|
-
params.push(filter.task_list_id);
|
|
8384
|
+
if (missing.length > 0) {
|
|
8385
|
+
throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
|
|
8288
8386
|
}
|
|
8289
|
-
|
|
8290
|
-
|
|
8291
|
-
|
|
8387
|
+
return merged;
|
|
8388
|
+
}
|
|
8389
|
+
function substituteVars(text, variables) {
|
|
8390
|
+
let result = text;
|
|
8391
|
+
for (const [key, val] of Object.entries(variables)) {
|
|
8392
|
+
result = result.replace(new RegExp(`\\{${key}\\}`, "g"), val);
|
|
8292
8393
|
}
|
|
8293
|
-
|
|
8294
|
-
const row = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
|
|
8295
|
-
return row.count;
|
|
8394
|
+
return result;
|
|
8296
8395
|
}
|
|
8297
|
-
function
|
|
8396
|
+
function tasksFromTemplate(templateId, projectId, variables, taskListId, db, _visitedTemplateIds) {
|
|
8298
8397
|
const d = db || getDatabase();
|
|
8299
|
-
const
|
|
8300
|
-
if (!
|
|
8301
|
-
throw new
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
const timestamp = now();
|
|
8306
|
-
const completionTimestamp = input.completed_at ?? timestamp;
|
|
8307
|
-
const sets = ["version = version + 1", "updated_at = ?"];
|
|
8308
|
-
const params = [timestamp];
|
|
8309
|
-
if (input.title !== undefined) {
|
|
8310
|
-
sets.push("title = ?");
|
|
8311
|
-
params.push(input.title);
|
|
8398
|
+
const template = getTemplateWithTasks(templateId, d);
|
|
8399
|
+
if (!template)
|
|
8400
|
+
throw new Error(`Template not found: ${templateId}`);
|
|
8401
|
+
const visited = _visitedTemplateIds || new Set;
|
|
8402
|
+
if (visited.has(template.id)) {
|
|
8403
|
+
throw new Error(`Circular template reference detected: ${template.id}`);
|
|
8312
8404
|
}
|
|
8313
|
-
|
|
8314
|
-
|
|
8315
|
-
|
|
8405
|
+
visited.add(template.id);
|
|
8406
|
+
const resolved = resolveVariables(template.variables, variables);
|
|
8407
|
+
if (template.tasks.length === 0) {
|
|
8408
|
+
const input = taskFromTemplate(templateId, { project_id: projectId, task_list_id: taskListId }, d);
|
|
8409
|
+
const task = createTask(input, d);
|
|
8410
|
+
return [task];
|
|
8316
8411
|
}
|
|
8317
|
-
|
|
8318
|
-
|
|
8319
|
-
|
|
8412
|
+
const createdTasks = [];
|
|
8413
|
+
const positionToId = new Map;
|
|
8414
|
+
const skippedPositions = new Set;
|
|
8415
|
+
for (const tt of template.tasks) {
|
|
8416
|
+
if (tt.include_template_id) {
|
|
8417
|
+
const includedTasks = tasksFromTemplate(tt.include_template_id, projectId, resolved, taskListId, d, visited);
|
|
8418
|
+
createdTasks.push(...includedTasks);
|
|
8419
|
+
if (includedTasks.length > 0) {
|
|
8420
|
+
positionToId.set(tt.position, includedTasks[0].id);
|
|
8421
|
+
} else {
|
|
8422
|
+
skippedPositions.add(tt.position);
|
|
8423
|
+
}
|
|
8424
|
+
continue;
|
|
8320
8425
|
}
|
|
8321
|
-
|
|
8322
|
-
|
|
8323
|
-
|
|
8324
|
-
sets.push("completed_at = ?");
|
|
8325
|
-
params.push(completionTimestamp);
|
|
8426
|
+
if (tt.condition && !evaluateCondition(tt.condition, resolved)) {
|
|
8427
|
+
skippedPositions.add(tt.position);
|
|
8428
|
+
continue;
|
|
8326
8429
|
}
|
|
8430
|
+
let title = tt.title_pattern;
|
|
8431
|
+
let desc = tt.description;
|
|
8432
|
+
title = substituteVars(title, resolved);
|
|
8433
|
+
if (desc)
|
|
8434
|
+
desc = substituteVars(desc, resolved);
|
|
8435
|
+
const task = createTask({
|
|
8436
|
+
title,
|
|
8437
|
+
description: desc ?? undefined,
|
|
8438
|
+
priority: tt.priority,
|
|
8439
|
+
tags: tt.tags,
|
|
8440
|
+
task_type: tt.task_type ?? undefined,
|
|
8441
|
+
project_id: projectId,
|
|
8442
|
+
task_list_id: taskListId,
|
|
8443
|
+
metadata: tt.metadata
|
|
8444
|
+
}, d);
|
|
8445
|
+
createdTasks.push(task);
|
|
8446
|
+
positionToId.set(tt.position, task.id);
|
|
8327
8447
|
}
|
|
8328
|
-
|
|
8329
|
-
|
|
8330
|
-
|
|
8331
|
-
|
|
8332
|
-
|
|
8333
|
-
|
|
8334
|
-
|
|
8335
|
-
|
|
8336
|
-
|
|
8337
|
-
|
|
8338
|
-
|
|
8339
|
-
|
|
8340
|
-
|
|
8341
|
-
|
|
8342
|
-
|
|
8343
|
-
}
|
|
8344
|
-
if (input.tags !== undefined) {
|
|
8345
|
-
sets.push("tags = ?");
|
|
8346
|
-
params.push(JSON.stringify(input.tags));
|
|
8347
|
-
}
|
|
8348
|
-
if (input.metadata !== undefined) {
|
|
8349
|
-
sets.push("metadata = ?");
|
|
8350
|
-
params.push(JSON.stringify(input.metadata));
|
|
8351
|
-
}
|
|
8352
|
-
if (input.plan_id !== undefined) {
|
|
8353
|
-
sets.push("plan_id = ?");
|
|
8354
|
-
params.push(input.plan_id);
|
|
8355
|
-
}
|
|
8356
|
-
if (input.task_list_id !== undefined) {
|
|
8357
|
-
sets.push("task_list_id = ?");
|
|
8358
|
-
params.push(input.task_list_id);
|
|
8359
|
-
}
|
|
8360
|
-
if (input.due_at !== undefined) {
|
|
8361
|
-
sets.push("due_at = ?");
|
|
8362
|
-
params.push(input.due_at);
|
|
8363
|
-
}
|
|
8364
|
-
if (input.estimated_minutes !== undefined) {
|
|
8365
|
-
sets.push("estimated_minutes = ?");
|
|
8366
|
-
params.push(input.estimated_minutes);
|
|
8367
|
-
}
|
|
8368
|
-
if (input.sla_minutes !== undefined) {
|
|
8369
|
-
sets.push("sla_minutes = ?");
|
|
8370
|
-
params.push(input.sla_minutes);
|
|
8371
|
-
}
|
|
8372
|
-
if (input.actual_minutes !== undefined) {
|
|
8373
|
-
sets.push("actual_minutes = ?");
|
|
8374
|
-
params.push(input.actual_minutes);
|
|
8375
|
-
}
|
|
8376
|
-
if (input.completed_at !== undefined && input.status !== "completed") {
|
|
8377
|
-
sets.push("completed_at = ?");
|
|
8378
|
-
params.push(input.completed_at);
|
|
8379
|
-
}
|
|
8380
|
-
if (input.confidence !== undefined) {
|
|
8381
|
-
sets.push("confidence = ?");
|
|
8382
|
-
params.push(input.confidence);
|
|
8383
|
-
}
|
|
8384
|
-
if (input.retry_count !== undefined) {
|
|
8385
|
-
sets.push("retry_count = ?");
|
|
8386
|
-
params.push(input.retry_count);
|
|
8387
|
-
}
|
|
8388
|
-
if (input.max_retries !== undefined) {
|
|
8389
|
-
sets.push("max_retries = ?");
|
|
8390
|
-
params.push(input.max_retries);
|
|
8391
|
-
}
|
|
8392
|
-
if (input.retry_after !== undefined) {
|
|
8393
|
-
sets.push("retry_after = ?");
|
|
8394
|
-
params.push(input.retry_after);
|
|
8395
|
-
}
|
|
8396
|
-
if (input.requires_approval !== undefined) {
|
|
8397
|
-
sets.push("requires_approval = ?");
|
|
8398
|
-
params.push(input.requires_approval ? 1 : 0);
|
|
8399
|
-
}
|
|
8400
|
-
if (input.approved_by !== undefined) {
|
|
8401
|
-
sets.push("approved_by = ?");
|
|
8402
|
-
params.push(input.approved_by);
|
|
8403
|
-
sets.push("approved_at = ?");
|
|
8404
|
-
params.push(now());
|
|
8405
|
-
}
|
|
8406
|
-
if (input.recurrence_rule !== undefined) {
|
|
8407
|
-
sets.push("recurrence_rule = ?");
|
|
8408
|
-
params.push(input.recurrence_rule);
|
|
8409
|
-
}
|
|
8410
|
-
if (input.task_type !== undefined) {
|
|
8411
|
-
sets.push("task_type = ?");
|
|
8412
|
-
params.push(input.task_type ?? null);
|
|
8413
|
-
}
|
|
8414
|
-
params.push(id, input.version);
|
|
8415
|
-
const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
|
|
8416
|
-
if (result.changes === 0) {
|
|
8417
|
-
const current = getTask(id, d);
|
|
8418
|
-
throw new VersionConflictError(id, input.version, current?.version ?? -1);
|
|
8419
|
-
}
|
|
8420
|
-
if (input.tags !== undefined) {
|
|
8421
|
-
replaceTaskTags(id, input.tags, d);
|
|
8422
|
-
}
|
|
8423
|
-
const agentId = task.assigned_to || task.agent_id || null;
|
|
8424
|
-
if (input.status !== undefined && input.status !== task.status)
|
|
8425
|
-
logTaskChange(id, "update", "status", task.status, input.status, agentId, d);
|
|
8426
|
-
if (input.priority !== undefined && input.priority !== task.priority)
|
|
8427
|
-
logTaskChange(id, "update", "priority", task.priority, input.priority, agentId, d);
|
|
8428
|
-
if (input.title !== undefined && input.title !== task.title)
|
|
8429
|
-
logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
|
|
8430
|
-
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
|
|
8431
|
-
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
8432
|
-
if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
|
|
8433
|
-
logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
|
|
8434
|
-
if (input.approved_by !== undefined)
|
|
8435
|
-
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
8436
|
-
const updatedTask = {
|
|
8437
|
-
...task,
|
|
8438
|
-
...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
|
|
8439
|
-
tags: input.tags ?? task.tags,
|
|
8440
|
-
metadata: input.metadata ?? task.metadata,
|
|
8441
|
-
version: task.version + 1,
|
|
8442
|
-
updated_at: timestamp,
|
|
8443
|
-
completed_at: input.status === "completed" ? completionTimestamp : input.completed_at !== undefined ? input.completed_at : task.completed_at,
|
|
8444
|
-
sla_minutes: input.sla_minutes !== undefined ? input.sla_minutes : task.sla_minutes,
|
|
8445
|
-
actual_minutes: input.actual_minutes ?? task.actual_minutes,
|
|
8446
|
-
confidence: input.confidence !== undefined ? input.confidence : task.confidence,
|
|
8447
|
-
retry_count: input.retry_count ?? task.retry_count,
|
|
8448
|
-
max_retries: input.max_retries ?? task.max_retries,
|
|
8449
|
-
retry_after: input.retry_after !== undefined ? input.retry_after : task.retry_after,
|
|
8450
|
-
requires_approval: input.requires_approval !== undefined ? input.requires_approval : task.requires_approval,
|
|
8451
|
-
approved_by: input.approved_by ?? task.approved_by,
|
|
8452
|
-
approved_at: input.approved_by ? timestamp : task.approved_at
|
|
8453
|
-
};
|
|
8454
|
-
const databasePath = databasePathFromDatabase(d);
|
|
8455
|
-
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
|
|
8456
|
-
const payload = taskEventData(updatedTask, { assigned_to: input.assigned_to, old_assigned_to: task.assigned_to });
|
|
8457
|
-
dispatchWebhook2("task.assigned", payload, d).catch(() => {});
|
|
8458
|
-
emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
|
|
8459
|
-
emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to }, databasePath });
|
|
8460
|
-
}
|
|
8461
|
-
if (input.status !== undefined && input.status !== task.status) {
|
|
8462
|
-
const payload = taskEventData(updatedTask, { old_status: task.status, new_status: input.status });
|
|
8463
|
-
dispatchWebhook2("task.status_changed", payload, d).catch(() => {});
|
|
8464
|
-
emitLocalEventHooksQuiet({ type: "task.status_changed", payload, databasePath });
|
|
8465
|
-
emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status }, databasePath });
|
|
8466
|
-
}
|
|
8467
|
-
if (input.approved_by !== undefined) {
|
|
8468
|
-
emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title }, databasePath });
|
|
8448
|
+
for (const tt of template.tasks) {
|
|
8449
|
+
if (skippedPositions.has(tt.position))
|
|
8450
|
+
continue;
|
|
8451
|
+
if (tt.include_template_id)
|
|
8452
|
+
continue;
|
|
8453
|
+
const deps = tt.depends_on_positions;
|
|
8454
|
+
for (const depPos of deps) {
|
|
8455
|
+
if (skippedPositions.has(depPos))
|
|
8456
|
+
continue;
|
|
8457
|
+
const taskId = positionToId.get(tt.position);
|
|
8458
|
+
const depId = positionToId.get(depPos);
|
|
8459
|
+
if (taskId && depId) {
|
|
8460
|
+
addDependency(taskId, depId, d);
|
|
8461
|
+
}
|
|
8462
|
+
}
|
|
8469
8463
|
}
|
|
8470
|
-
|
|
8471
|
-
dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
|
|
8472
|
-
emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload, databasePath });
|
|
8473
|
-
emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask, databasePath });
|
|
8474
|
-
return updatedTask;
|
|
8464
|
+
return createdTasks;
|
|
8475
8465
|
}
|
|
8476
|
-
function
|
|
8466
|
+
function previewTemplate(templateId, variables, db) {
|
|
8477
8467
|
const d = db || getDatabase();
|
|
8478
|
-
const
|
|
8479
|
-
if (!
|
|
8480
|
-
|
|
8481
|
-
|
|
8482
|
-
|
|
8483
|
-
|
|
8484
|
-
|
|
8485
|
-
|
|
8486
|
-
|
|
8487
|
-
|
|
8488
|
-
|
|
8468
|
+
const template = getTemplateWithTasks(templateId, d);
|
|
8469
|
+
if (!template)
|
|
8470
|
+
throw new Error(`Template not found: ${templateId}`);
|
|
8471
|
+
const resolved = resolveVariables(template.variables, variables);
|
|
8472
|
+
const tasks = [];
|
|
8473
|
+
if (template.tasks.length === 0) {
|
|
8474
|
+
tasks.push({
|
|
8475
|
+
position: 0,
|
|
8476
|
+
title: substituteVars(template.title_pattern, resolved),
|
|
8477
|
+
description: template.description ? substituteVars(template.description, resolved) : null,
|
|
8478
|
+
priority: template.priority,
|
|
8479
|
+
tags: template.tags,
|
|
8480
|
+
task_type: null,
|
|
8481
|
+
depends_on_positions: []
|
|
8482
|
+
});
|
|
8483
|
+
} else {
|
|
8484
|
+
for (const tt of template.tasks) {
|
|
8485
|
+
if (tt.condition && !evaluateCondition(tt.condition, resolved))
|
|
8486
|
+
continue;
|
|
8487
|
+
tasks.push({
|
|
8488
|
+
position: tt.position,
|
|
8489
|
+
title: substituteVars(tt.title_pattern, resolved),
|
|
8490
|
+
description: tt.description ? substituteVars(tt.description, resolved) : null,
|
|
8491
|
+
priority: tt.priority,
|
|
8492
|
+
tags: tt.tags,
|
|
8493
|
+
task_type: tt.task_type,
|
|
8494
|
+
depends_on_positions: tt.depends_on_positions
|
|
8495
|
+
});
|
|
8496
|
+
}
|
|
8497
|
+
}
|
|
8498
|
+
return {
|
|
8499
|
+
template_id: template.id,
|
|
8500
|
+
template_name: template.name,
|
|
8501
|
+
description: template.description,
|
|
8502
|
+
variables: template.variables,
|
|
8503
|
+
resolved_variables: resolved,
|
|
8504
|
+
tasks
|
|
8505
|
+
};
|
|
8489
8506
|
}
|
|
8490
|
-
var
|
|
8491
|
-
init_types();
|
|
8507
|
+
var init_templates = __esm(() => {
|
|
8492
8508
|
init_database();
|
|
8493
|
-
|
|
8494
|
-
init_event_emission_safety();
|
|
8495
|
-
init_event_hooks();
|
|
8496
|
-
init_shared_events();
|
|
8497
|
-
init_audit();
|
|
8498
|
-
init_webhooks();
|
|
8499
|
-
init_checklists();
|
|
8509
|
+
init_tasks();
|
|
8500
8510
|
init_storage_tombstones();
|
|
8501
8511
|
});
|
|
8502
8512
|
|
|
8503
|
-
// src/
|
|
8504
|
-
function
|
|
8505
|
-
const
|
|
8506
|
-
if (
|
|
8507
|
-
|
|
8508
|
-
|
|
8509
|
-
|
|
8510
|
-
|
|
8511
|
-
|
|
8512
|
-
if (normalized === "every week" || normalized === "weekly") {
|
|
8513
|
-
return { type: "interval", interval: 1, unit: "week" };
|
|
8514
|
-
}
|
|
8515
|
-
if (normalized === "every month" || normalized === "monthly") {
|
|
8516
|
-
return { type: "interval", interval: 1, unit: "month" };
|
|
8517
|
-
}
|
|
8518
|
-
const intervalMatch = normalized.match(/^every\s+(\d+)\s+(day|week|month)s?$/);
|
|
8519
|
-
if (intervalMatch) {
|
|
8520
|
-
return {
|
|
8521
|
-
type: "interval",
|
|
8522
|
-
interval: parseInt(intervalMatch[1], 10),
|
|
8523
|
-
unit: intervalMatch[2]
|
|
8524
|
-
};
|
|
8525
|
-
}
|
|
8526
|
-
const daysMatch = normalized.match(/^every\s+(.+)$/);
|
|
8527
|
-
if (daysMatch) {
|
|
8528
|
-
const dayParts = daysMatch[1].split(/[,\s]+/).map((d) => d.trim()).filter(Boolean);
|
|
8529
|
-
const days = [];
|
|
8530
|
-
for (const part of dayParts) {
|
|
8531
|
-
const dayNum = DAY_NAMES[part];
|
|
8532
|
-
if (dayNum !== undefined) {
|
|
8533
|
-
days.push(dayNum);
|
|
8534
|
-
}
|
|
8535
|
-
}
|
|
8536
|
-
if (days.length > 0) {
|
|
8537
|
-
return { type: "specific_days", days: days.sort((a, b) => a - b) };
|
|
8538
|
-
}
|
|
8513
|
+
// src/db/task-graph.ts
|
|
8514
|
+
function addDependency(taskId, dependsOn, db) {
|
|
8515
|
+
const d = db || getDatabase();
|
|
8516
|
+
if (!getTask(taskId, d))
|
|
8517
|
+
throw new TaskNotFoundError(taskId);
|
|
8518
|
+
if (!getTask(dependsOn, d))
|
|
8519
|
+
throw new TaskNotFoundError(dependsOn);
|
|
8520
|
+
if (wouldCreateCycle(taskId, dependsOn, d)) {
|
|
8521
|
+
throw new DependencyCycleError(taskId, dependsOn);
|
|
8539
8522
|
}
|
|
8540
|
-
|
|
8523
|
+
d.run("INSERT OR IGNORE INTO task_dependencies (task_id, depends_on) VALUES (?, ?)", [taskId, dependsOn]);
|
|
8541
8524
|
}
|
|
8542
|
-
function
|
|
8543
|
-
|
|
8544
|
-
|
|
8545
|
-
|
|
8546
|
-
} catch {
|
|
8547
|
-
return false;
|
|
8548
|
-
}
|
|
8525
|
+
function removeDependency(taskId, dependsOn, db) {
|
|
8526
|
+
const d = db || getDatabase();
|
|
8527
|
+
const result = d.run("DELETE FROM task_dependencies WHERE task_id = ? AND depends_on = ?", [taskId, dependsOn]);
|
|
8528
|
+
return result.changes > 0;
|
|
8549
8529
|
}
|
|
8550
|
-
function
|
|
8551
|
-
const
|
|
8552
|
-
|
|
8553
|
-
if (parsed.type === "interval") {
|
|
8554
|
-
const next = new Date(base);
|
|
8555
|
-
if (parsed.unit === "day") {
|
|
8556
|
-
next.setDate(next.getDate() + parsed.interval);
|
|
8557
|
-
} else if (parsed.unit === "week") {
|
|
8558
|
-
next.setDate(next.getDate() + parsed.interval * 7);
|
|
8559
|
-
} else if (parsed.unit === "month") {
|
|
8560
|
-
next.setMonth(next.getMonth() + parsed.interval);
|
|
8561
|
-
}
|
|
8562
|
-
return next.toISOString();
|
|
8563
|
-
}
|
|
8564
|
-
if (parsed.type === "specific_days") {
|
|
8565
|
-
const currentDay = base.getDay();
|
|
8566
|
-
const days = parsed.days;
|
|
8567
|
-
let daysToAdd = Infinity;
|
|
8568
|
-
for (const day of days) {
|
|
8569
|
-
let diff = day - currentDay;
|
|
8570
|
-
if (diff <= 0)
|
|
8571
|
-
diff += 7;
|
|
8572
|
-
if (diff < daysToAdd)
|
|
8573
|
-
daysToAdd = diff;
|
|
8574
|
-
}
|
|
8575
|
-
const next = new Date(base);
|
|
8576
|
-
next.setDate(next.getDate() + daysToAdd);
|
|
8577
|
-
return next.toISOString();
|
|
8578
|
-
}
|
|
8579
|
-
throw new Error(`Cannot calculate next occurrence for rule: "${rule}"`);
|
|
8530
|
+
function getTaskDependencies(taskId, db) {
|
|
8531
|
+
const d = db || getDatabase();
|
|
8532
|
+
return d.query("SELECT * FROM task_dependencies WHERE task_id = ?").all(taskId);
|
|
8580
8533
|
}
|
|
8581
|
-
|
|
8582
|
-
|
|
8583
|
-
|
|
8584
|
-
sunday: 0,
|
|
8585
|
-
sun: 0,
|
|
8586
|
-
monday: 1,
|
|
8587
|
-
mon: 1,
|
|
8588
|
-
tuesday: 2,
|
|
8589
|
-
tue: 2,
|
|
8590
|
-
wednesday: 3,
|
|
8591
|
-
wed: 3,
|
|
8592
|
-
thursday: 4,
|
|
8593
|
-
thu: 4,
|
|
8594
|
-
friday: 5,
|
|
8595
|
-
fri: 5,
|
|
8596
|
-
saturday: 6,
|
|
8597
|
-
sat: 6
|
|
8598
|
-
};
|
|
8599
|
-
});
|
|
8600
|
-
|
|
8601
|
-
// src/db/templates.ts
|
|
8602
|
-
var exports_templates = {};
|
|
8603
|
-
__export(exports_templates, {
|
|
8604
|
-
updateTemplate: () => updateTemplate,
|
|
8605
|
-
tasksFromTemplate: () => tasksFromTemplate,
|
|
8606
|
-
taskFromTemplate: () => taskFromTemplate,
|
|
8607
|
-
resolveVariables: () => resolveVariables,
|
|
8608
|
-
previewTemplate: () => previewTemplate,
|
|
8609
|
-
listTemplates: () => listTemplates,
|
|
8610
|
-
listTemplateVersions: () => listTemplateVersions,
|
|
8611
|
-
importTemplate: () => importTemplate,
|
|
8612
|
-
getTemplateWithTasks: () => getTemplateWithTasks,
|
|
8613
|
-
getTemplateVersion: () => getTemplateVersion,
|
|
8614
|
-
getTemplateTasks: () => getTemplateTasks,
|
|
8615
|
-
getTemplate: () => getTemplate,
|
|
8616
|
-
exportTemplate: () => exportTemplate,
|
|
8617
|
-
evaluateCondition: () => evaluateCondition,
|
|
8618
|
-
deleteTemplate: () => deleteTemplate,
|
|
8619
|
-
createTemplate: () => createTemplate,
|
|
8620
|
-
addTemplateTasks: () => addTemplateTasks
|
|
8621
|
-
});
|
|
8622
|
-
function rowToTemplate(row) {
|
|
8623
|
-
return {
|
|
8624
|
-
...row,
|
|
8625
|
-
tags: JSON.parse(row.tags || "[]"),
|
|
8626
|
-
variables: JSON.parse(row.variables || "[]"),
|
|
8627
|
-
metadata: JSON.parse(row.metadata || "{}"),
|
|
8628
|
-
priority: row.priority || "medium",
|
|
8629
|
-
version: row.version ?? 1
|
|
8630
|
-
};
|
|
8534
|
+
function getTaskDependents(taskId, db) {
|
|
8535
|
+
const d = db || getDatabase();
|
|
8536
|
+
return d.query("SELECT * FROM task_dependencies WHERE depends_on = ?").all(taskId);
|
|
8631
8537
|
}
|
|
8632
|
-
function
|
|
8633
|
-
|
|
8634
|
-
|
|
8635
|
-
|
|
8636
|
-
|
|
8637
|
-
|
|
8638
|
-
|
|
8639
|
-
|
|
8640
|
-
|
|
8538
|
+
function cloneTask(taskId, overrides, db) {
|
|
8539
|
+
const d = db || getDatabase();
|
|
8540
|
+
const source = getTask(taskId, d);
|
|
8541
|
+
if (!source)
|
|
8542
|
+
throw new TaskNotFoundError(taskId);
|
|
8543
|
+
const input = {
|
|
8544
|
+
title: overrides?.title ?? source.title,
|
|
8545
|
+
description: overrides?.description ?? source.description ?? undefined,
|
|
8546
|
+
priority: overrides?.priority ?? source.priority,
|
|
8547
|
+
project_id: overrides?.project_id ?? source.project_id ?? undefined,
|
|
8548
|
+
parent_id: overrides?.parent_id ?? source.parent_id ?? undefined,
|
|
8549
|
+
plan_id: overrides?.plan_id ?? source.plan_id ?? undefined,
|
|
8550
|
+
task_list_id: overrides?.task_list_id ?? source.task_list_id ?? undefined,
|
|
8551
|
+
status: overrides?.status ?? "pending",
|
|
8552
|
+
agent_id: overrides?.agent_id ?? source.agent_id ?? undefined,
|
|
8553
|
+
assigned_to: overrides?.assigned_to ?? source.assigned_to ?? undefined,
|
|
8554
|
+
tags: overrides?.tags ?? source.tags,
|
|
8555
|
+
metadata: overrides?.metadata ?? source.metadata,
|
|
8556
|
+
estimated_minutes: overrides?.estimated_minutes ?? source.estimated_minutes ?? undefined,
|
|
8557
|
+
recurrence_rule: overrides?.recurrence_rule ?? source.recurrence_rule ?? undefined
|
|
8641
8558
|
};
|
|
8559
|
+
return createTask(input, d);
|
|
8642
8560
|
}
|
|
8643
|
-
function
|
|
8644
|
-
return resolvePartialId(d, "task_templates", id);
|
|
8645
|
-
}
|
|
8646
|
-
function createTemplate(input, db) {
|
|
8561
|
+
function getTaskGraph(taskId, direction = "both", db) {
|
|
8647
8562
|
const d = db || getDatabase();
|
|
8648
|
-
const
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
id,
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
8661
|
-
|
|
8662
|
-
|
|
8663
|
-
|
|
8664
|
-
|
|
8665
|
-
|
|
8666
|
-
|
|
8563
|
+
const task = getTask(taskId, d);
|
|
8564
|
+
if (!task)
|
|
8565
|
+
throw new TaskNotFoundError(taskId);
|
|
8566
|
+
function toNode(t) {
|
|
8567
|
+
const deps = getTaskDependencies(t.id, d);
|
|
8568
|
+
const hasUnfinishedDeps = deps.some((dep) => {
|
|
8569
|
+
const depTask = getTask(dep.depends_on, d);
|
|
8570
|
+
return depTask && depTask.status !== "completed";
|
|
8571
|
+
});
|
|
8572
|
+
return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
|
|
8573
|
+
}
|
|
8574
|
+
function buildUp(id, visited) {
|
|
8575
|
+
if (visited.has(id))
|
|
8576
|
+
return [];
|
|
8577
|
+
visited.add(id);
|
|
8578
|
+
const deps = d.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(id);
|
|
8579
|
+
return deps.map((dep) => {
|
|
8580
|
+
const depTask = getTask(dep.depends_on, d);
|
|
8581
|
+
if (!depTask)
|
|
8582
|
+
return null;
|
|
8583
|
+
return { task: toNode(depTask), depends_on: buildUp(dep.depends_on, visited), blocks: [] };
|
|
8584
|
+
}).filter(Boolean);
|
|
8585
|
+
}
|
|
8586
|
+
function buildDown(id, visited) {
|
|
8587
|
+
if (visited.has(id))
|
|
8588
|
+
return [];
|
|
8589
|
+
visited.add(id);
|
|
8590
|
+
const dependents = d.query("SELECT task_id FROM task_dependencies WHERE depends_on = ?").all(id);
|
|
8591
|
+
return dependents.map((dep) => {
|
|
8592
|
+
const depTask = getTask(dep.task_id, d);
|
|
8593
|
+
if (!depTask)
|
|
8594
|
+
return null;
|
|
8595
|
+
return { task: toNode(depTask), depends_on: [], blocks: buildDown(dep.task_id, visited) };
|
|
8596
|
+
}).filter(Boolean);
|
|
8667
8597
|
}
|
|
8668
|
-
|
|
8598
|
+
const rootNode = toNode(task);
|
|
8599
|
+
const depends_on = direction === "up" || direction === "both" ? buildUp(taskId, new Set) : [];
|
|
8600
|
+
const blocks = direction === "down" || direction === "both" ? buildDown(taskId, new Set) : [];
|
|
8601
|
+
return { task: rootNode, depends_on, blocks };
|
|
8669
8602
|
}
|
|
8670
|
-
function
|
|
8603
|
+
function moveTask(taskId, target, db) {
|
|
8671
8604
|
const d = db || getDatabase();
|
|
8672
|
-
const
|
|
8673
|
-
if (!
|
|
8605
|
+
const task = getTask(taskId, d);
|
|
8606
|
+
if (!task)
|
|
8607
|
+
throw new TaskNotFoundError(taskId);
|
|
8608
|
+
const sets = ["updated_at = ?", "version = version + 1"];
|
|
8609
|
+
const params = [now()];
|
|
8610
|
+
if (target.task_list_id !== undefined) {
|
|
8611
|
+
sets.push("task_list_id = ?");
|
|
8612
|
+
params.push(target.task_list_id);
|
|
8613
|
+
}
|
|
8614
|
+
if (target.project_id !== undefined) {
|
|
8615
|
+
sets.push("project_id = ?");
|
|
8616
|
+
params.push(target.project_id);
|
|
8617
|
+
}
|
|
8618
|
+
if (target.plan_id !== undefined) {
|
|
8619
|
+
sets.push("plan_id = ?");
|
|
8620
|
+
params.push(target.plan_id);
|
|
8621
|
+
}
|
|
8622
|
+
params.push(taskId);
|
|
8623
|
+
d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
8624
|
+
return getTask(taskId, d);
|
|
8625
|
+
}
|
|
8626
|
+
function wouldCreateCycle(taskId, dependsOn, db) {
|
|
8627
|
+
const visited = new Set;
|
|
8628
|
+
const queue = [dependsOn];
|
|
8629
|
+
while (queue.length > 0) {
|
|
8630
|
+
const current = queue.shift();
|
|
8631
|
+
if (current === taskId)
|
|
8632
|
+
return true;
|
|
8633
|
+
if (visited.has(current))
|
|
8634
|
+
continue;
|
|
8635
|
+
visited.add(current);
|
|
8636
|
+
const deps = db.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(current);
|
|
8637
|
+
for (const dep of deps) {
|
|
8638
|
+
queue.push(dep.depends_on);
|
|
8639
|
+
}
|
|
8640
|
+
}
|
|
8641
|
+
return false;
|
|
8642
|
+
}
|
|
8643
|
+
var init_task_graph = __esm(() => {
|
|
8644
|
+
init_types();
|
|
8645
|
+
init_database();
|
|
8646
|
+
init_task_crud();
|
|
8647
|
+
});
|
|
8648
|
+
|
|
8649
|
+
// src/db/task-lifecycle.ts
|
|
8650
|
+
var exports_task_lifecycle = {};
|
|
8651
|
+
__export(exports_task_lifecycle, {
|
|
8652
|
+
unlockTask: () => unlockTask,
|
|
8653
|
+
stealTask: () => stealTask,
|
|
8654
|
+
startTask: () => startTask,
|
|
8655
|
+
spawnNextRecurrence: () => spawnNextRecurrence,
|
|
8656
|
+
lockTask: () => lockTask,
|
|
8657
|
+
getTasksChangedSince: () => getTasksChangedSince,
|
|
8658
|
+
getTaskLockStatus: () => getTaskLockStatus,
|
|
8659
|
+
getStaleTasks: () => getStaleTasks,
|
|
8660
|
+
getNextTask: () => getNextTask,
|
|
8661
|
+
getBlockingDeps: () => getBlockingDeps,
|
|
8662
|
+
getActiveWork: () => getActiveWork,
|
|
8663
|
+
failTask: () => failTask,
|
|
8664
|
+
completeTask: () => completeTask,
|
|
8665
|
+
claimOrSteal: () => claimOrSteal,
|
|
8666
|
+
claimNextTask: () => claimNextTask
|
|
8667
|
+
});
|
|
8668
|
+
function lockExpiresAt(lockedAt) {
|
|
8669
|
+
if (!lockedAt)
|
|
8674
8670
|
return null;
|
|
8675
|
-
|
|
8676
|
-
return row ? rowToTemplate(row) : null;
|
|
8671
|
+
return new Date(new Date(lockedAt).getTime() + LOCK_EXPIRY_MINUTES * 60 * 1000).toISOString();
|
|
8677
8672
|
}
|
|
8678
|
-
function
|
|
8679
|
-
|
|
8680
|
-
|
|
8673
|
+
function assertStartable(task, agentId) {
|
|
8674
|
+
if (task.status === "pending")
|
|
8675
|
+
return;
|
|
8676
|
+
if (task.status === "in_progress")
|
|
8677
|
+
return;
|
|
8678
|
+
throw new Error(`Task is ${task.status} and cannot be started by ${agentId}`);
|
|
8681
8679
|
}
|
|
8682
|
-
function
|
|
8680
|
+
function getBlockingDeps(id, db) {
|
|
8683
8681
|
const d = db || getDatabase();
|
|
8684
|
-
const
|
|
8685
|
-
if (
|
|
8686
|
-
return
|
|
8687
|
-
const
|
|
8688
|
-
|
|
8689
|
-
|
|
8690
|
-
|
|
8691
|
-
|
|
8692
|
-
|
|
8693
|
-
|
|
8694
|
-
version: template.version
|
|
8695
|
-
}, d);
|
|
8696
|
-
return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
|
|
8682
|
+
const deps = getTaskDependencies(id, d);
|
|
8683
|
+
if (deps.length === 0)
|
|
8684
|
+
return [];
|
|
8685
|
+
const blocking = [];
|
|
8686
|
+
for (const dep of deps) {
|
|
8687
|
+
const task = getTask(dep.depends_on, d);
|
|
8688
|
+
if (task && task.status !== "completed")
|
|
8689
|
+
blocking.push(task);
|
|
8690
|
+
}
|
|
8691
|
+
return blocking;
|
|
8697
8692
|
}
|
|
8698
|
-
function
|
|
8693
|
+
function startTask(id, agentId, db) {
|
|
8699
8694
|
const d = db || getDatabase();
|
|
8700
|
-
const
|
|
8701
|
-
|
|
8702
|
-
|
|
8703
|
-
|
|
8704
|
-
|
|
8705
|
-
|
|
8706
|
-
|
|
8707
|
-
|
|
8708
|
-
|
|
8709
|
-
|
|
8710
|
-
|
|
8711
|
-
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
|
|
8715
|
-
|
|
8695
|
+
const databasePath = databasePathFromDatabase(d);
|
|
8696
|
+
const task = getTask(id, d);
|
|
8697
|
+
if (!task)
|
|
8698
|
+
throw new TaskNotFoundError(id);
|
|
8699
|
+
assertStartable(task, agentId);
|
|
8700
|
+
const blocking = getBlockingDeps(id, d);
|
|
8701
|
+
if (blocking.length > 0) {
|
|
8702
|
+
const blockerIds = blocking.map((b) => b.id.slice(0, 8)).join(", ");
|
|
8703
|
+
emitLocalEventHooksQuiet({
|
|
8704
|
+
type: "task.blocked",
|
|
8705
|
+
payload: {
|
|
8706
|
+
id,
|
|
8707
|
+
agent_id: agentId,
|
|
8708
|
+
title: task.title,
|
|
8709
|
+
blockers: blocking.map((b) => ({ id: b.id, short_id: b.short_id, title: b.title, status: b.status }))
|
|
8710
|
+
},
|
|
8711
|
+
databasePath
|
|
8716
8712
|
});
|
|
8717
|
-
|
|
8713
|
+
throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
|
|
8718
8714
|
}
|
|
8719
|
-
const
|
|
8720
|
-
const
|
|
8721
|
-
|
|
8722
|
-
|
|
8723
|
-
|
|
8715
|
+
const cutoff = lockExpiryCutoff();
|
|
8716
|
+
const timestamp = now();
|
|
8717
|
+
const result = d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, started_at = COALESCE(started_at, ?), version = version + 1, updated_at = ?
|
|
8718
|
+
WHERE id = ? AND status IN ('pending', 'in_progress') AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp, timestamp, timestamp, id, agentId, cutoff]);
|
|
8719
|
+
if (result.changes === 0) {
|
|
8720
|
+
const current = getTask(id, d);
|
|
8721
|
+
if (!current)
|
|
8722
|
+
throw new TaskNotFoundError(id);
|
|
8723
|
+
assertStartable(current, agentId);
|
|
8724
|
+
if (current.locked_by && current.locked_by !== agentId && !isLockExpired(current.locked_at)) {
|
|
8725
|
+
throw new LockError(id, current.locked_by);
|
|
8726
|
+
}
|
|
8727
|
+
throw new Error(`Task ${id} could not be started because it changed during claim`);
|
|
8724
8728
|
}
|
|
8725
|
-
|
|
8726
|
-
|
|
8727
|
-
|
|
8729
|
+
logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
|
|
8730
|
+
const startedTask = { ...task, status: "in_progress", assigned_to: agentId, locked_by: agentId, locked_at: timestamp, started_at: task.started_at || timestamp, version: task.version + 1, updated_at: timestamp };
|
|
8731
|
+
const payload = taskEventData(startedTask, { agent_id: agentId });
|
|
8732
|
+
dispatchWebhook2("task.started", payload, d).catch(() => {});
|
|
8733
|
+
emitLocalEventHooksQuiet({ type: "task.started", payload, databasePath });
|
|
8734
|
+
emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId }, databasePath });
|
|
8735
|
+
return startedTask;
|
|
8736
|
+
}
|
|
8737
|
+
function completeTask(id, agentId, db, options) {
|
|
8738
|
+
const d = db || getDatabase();
|
|
8739
|
+
const databasePath = databasePathFromDatabase(d);
|
|
8740
|
+
const task = getTask(id, d);
|
|
8741
|
+
if (!task)
|
|
8742
|
+
throw new TaskNotFoundError(id);
|
|
8743
|
+
if (task.status === "completed") {
|
|
8744
|
+
return task;
|
|
8728
8745
|
}
|
|
8729
|
-
if (
|
|
8730
|
-
|
|
8731
|
-
values.push(updates.description);
|
|
8746
|
+
if (task.status === "cancelled") {
|
|
8747
|
+
throw new Error(`Task ${id} is cancelled and cannot be completed`);
|
|
8732
8748
|
}
|
|
8733
|
-
if (
|
|
8734
|
-
|
|
8735
|
-
values.push(updates.priority);
|
|
8749
|
+
if (agentId && task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
|
|
8750
|
+
throw new LockError(id, task.locked_by);
|
|
8736
8751
|
}
|
|
8737
|
-
|
|
8738
|
-
|
|
8739
|
-
|
|
8752
|
+
checkCompletionGuard(task, agentId || null, d);
|
|
8753
|
+
const evidence = options ? { files_changed: options.files_changed, test_results: options.test_results, commit_hash: options.commit_hash, notes: options.notes, attachment_ids: options.attachment_ids } : undefined;
|
|
8754
|
+
const hasEvidence = evidence && (evidence.files_changed || evidence.test_results || evidence.commit_hash || evidence.notes || evidence.attachment_ids);
|
|
8755
|
+
const completionMeta = {};
|
|
8756
|
+
if (hasEvidence)
|
|
8757
|
+
completionMeta._evidence = evidence;
|
|
8758
|
+
if (options?.confidence !== undefined) {
|
|
8759
|
+
completionMeta._completion = { confidence: options.confidence };
|
|
8740
8760
|
}
|
|
8741
|
-
|
|
8742
|
-
|
|
8743
|
-
|
|
8761
|
+
const hasMeta = Object.keys(completionMeta).length > 0;
|
|
8762
|
+
const timestamp = options?.completed_at || now();
|
|
8763
|
+
const confidence = options?.confidence !== undefined ? options.confidence : task.confidence;
|
|
8764
|
+
const versionBeforeStatus = task.version + (hasMeta ? 1 : 0);
|
|
8765
|
+
const finalVersion = versionBeforeStatus + 1;
|
|
8766
|
+
const tx = d.transaction(() => {
|
|
8767
|
+
if (hasMeta) {
|
|
8768
|
+
const meta2 = { ...task.metadata, ...completionMeta };
|
|
8769
|
+
const metaResult = d.run("UPDATE tasks SET metadata = ?, version = version + 1, updated_at = ? WHERE id = ? AND version = ?", [JSON.stringify(meta2), timestamp, id, task.version]);
|
|
8770
|
+
if (metaResult.changes === 0) {
|
|
8771
|
+
const current = getTask(id, d);
|
|
8772
|
+
throw new VersionConflictError(id, task.version, current?.version ?? -1);
|
|
8773
|
+
}
|
|
8774
|
+
}
|
|
8775
|
+
const statusResult = d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, confidence = ?, version = version + 1, updated_at = ?
|
|
8776
|
+
WHERE id = ? AND version = ?`, [timestamp, confidence, timestamp, id, versionBeforeStatus]);
|
|
8777
|
+
if (statusResult.changes === 0) {
|
|
8778
|
+
const current = getTask(id, d);
|
|
8779
|
+
throw new VersionConflictError(id, versionBeforeStatus, current?.version ?? -1);
|
|
8780
|
+
}
|
|
8781
|
+
});
|
|
8782
|
+
tx();
|
|
8783
|
+
logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
|
|
8784
|
+
const completedTaskForEvent = {
|
|
8785
|
+
...task,
|
|
8786
|
+
status: "completed",
|
|
8787
|
+
locked_by: null,
|
|
8788
|
+
locked_at: null,
|
|
8789
|
+
completed_at: timestamp,
|
|
8790
|
+
confidence,
|
|
8791
|
+
version: finalVersion,
|
|
8792
|
+
updated_at: timestamp,
|
|
8793
|
+
metadata: hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata
|
|
8794
|
+
};
|
|
8795
|
+
const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
|
|
8796
|
+
dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
|
|
8797
|
+
emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload, databasePath });
|
|
8798
|
+
emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp }, databasePath });
|
|
8799
|
+
let spawnedTask = null;
|
|
8800
|
+
if (task.recurrence_rule && !options?.skip_recurrence) {
|
|
8801
|
+
try {
|
|
8802
|
+
spawnedTask = spawnNextRecurrence(task, d, timestamp);
|
|
8803
|
+
} catch (e) {
|
|
8804
|
+
spawnedTask = null;
|
|
8805
|
+
console.warn(`[tasks] failed to spawn next recurrence for ${id}: ${e instanceof Error ? e.message : String(e)}`);
|
|
8806
|
+
}
|
|
8744
8807
|
}
|
|
8745
|
-
|
|
8746
|
-
|
|
8747
|
-
|
|
8808
|
+
let spawnedFromTemplate = null;
|
|
8809
|
+
if (task.spawns_template_id) {
|
|
8810
|
+
const spawnDepth = task.metadata?._spawn_depth || 0;
|
|
8811
|
+
if (spawnDepth >= MAX_SPAWN_DEPTH) {
|
|
8812
|
+
console.warn(`[tasks] Task ${id} exceeded max spawn depth (${MAX_SPAWN_DEPTH}), skipping template spawn`);
|
|
8813
|
+
} else {
|
|
8814
|
+
try {
|
|
8815
|
+
const input = taskFromTemplate(task.spawns_template_id, {
|
|
8816
|
+
project_id: task.project_id ?? undefined,
|
|
8817
|
+
plan_id: task.plan_id ?? undefined,
|
|
8818
|
+
task_list_id: task.task_list_id ?? undefined,
|
|
8819
|
+
assigned_to: task.assigned_to ?? undefined
|
|
8820
|
+
}, d);
|
|
8821
|
+
input.metadata = { ...input.metadata || {}, _spawn_depth: spawnDepth + 1 };
|
|
8822
|
+
spawnedFromTemplate = createTask(input, d);
|
|
8823
|
+
} catch {}
|
|
8824
|
+
}
|
|
8748
8825
|
}
|
|
8749
|
-
|
|
8750
|
-
|
|
8751
|
-
|
|
8826
|
+
const meta = hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata;
|
|
8827
|
+
if (spawnedTask) {
|
|
8828
|
+
meta._next_recurrence = { id: spawnedTask.id, short_id: spawnedTask.short_id, due_at: spawnedTask.due_at };
|
|
8752
8829
|
}
|
|
8753
|
-
if (
|
|
8754
|
-
|
|
8755
|
-
values.push(JSON.stringify(updates.metadata));
|
|
8830
|
+
if (spawnedFromTemplate) {
|
|
8831
|
+
meta._spawned_task = { id: spawnedFromTemplate.id, short_id: spawnedFromTemplate.short_id, title: spawnedFromTemplate.title };
|
|
8756
8832
|
}
|
|
8757
|
-
|
|
8758
|
-
|
|
8759
|
-
|
|
8760
|
-
|
|
8761
|
-
|
|
8762
|
-
|
|
8763
|
-
|
|
8764
|
-
|
|
8765
|
-
|
|
8766
|
-
|
|
8767
|
-
|
|
8768
|
-
|
|
8769
|
-
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
|
|
8773
|
-
|
|
8774
|
-
|
|
8775
|
-
};
|
|
8776
|
-
}
|
|
8777
|
-
function addTemplateTasks(templateId, tasks, db) {
|
|
8778
|
-
const d = db || getDatabase();
|
|
8779
|
-
const template = getTemplate(templateId, d);
|
|
8780
|
-
if (!template)
|
|
8781
|
-
throw new Error(`Template not found: ${templateId}`);
|
|
8782
|
-
d.run("DELETE FROM template_tasks WHERE template_id = ?", [templateId]);
|
|
8783
|
-
const results = [];
|
|
8784
|
-
for (let i = 0;i < tasks.length; i++) {
|
|
8785
|
-
const task = tasks[i];
|
|
8786
|
-
const id = uuid();
|
|
8787
|
-
d.run(`INSERT INTO template_tasks (id, template_id, position, title_pattern, description, priority, tags, task_type, condition, include_template_id, depends_on_positions, metadata, created_at)
|
|
8788
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
8789
|
-
id,
|
|
8790
|
-
templateId,
|
|
8791
|
-
i,
|
|
8792
|
-
task.title_pattern,
|
|
8793
|
-
task.description || null,
|
|
8794
|
-
task.priority || "medium",
|
|
8795
|
-
JSON.stringify(task.tags || []),
|
|
8796
|
-
task.task_type || null,
|
|
8797
|
-
task.condition || null,
|
|
8798
|
-
task.include_template_id || null,
|
|
8799
|
-
JSON.stringify(task.depends_on || []),
|
|
8800
|
-
JSON.stringify(task.metadata || {}),
|
|
8801
|
-
now()
|
|
8802
|
-
]);
|
|
8803
|
-
const row = d.query("SELECT * FROM template_tasks WHERE id = ?").get(id);
|
|
8804
|
-
if (row)
|
|
8805
|
-
results.push(rowToTemplateTask(row));
|
|
8833
|
+
const unblockedDeps = d.query(`SELECT DISTINCT t.id, t.short_id, t.title FROM tasks t
|
|
8834
|
+
JOIN task_dependencies td ON td.task_id = t.id
|
|
8835
|
+
WHERE td.depends_on = ? AND t.status = 'pending'
|
|
8836
|
+
AND NOT EXISTS (
|
|
8837
|
+
SELECT 1 FROM task_dependencies td2
|
|
8838
|
+
JOIN tasks dep2 ON dep2.id = td2.depends_on
|
|
8839
|
+
WHERE td2.task_id = t.id AND dep2.status NOT IN ('completed', 'cancelled') AND dep2.id != ?
|
|
8840
|
+
)`).all(id, id);
|
|
8841
|
+
if (unblockedDeps.length > 0) {
|
|
8842
|
+
meta._unblocked = unblockedDeps.map((d2) => ({ id: d2.id, short_id: d2.short_id, title: d2.title }));
|
|
8843
|
+
for (const dep of unblockedDeps) {
|
|
8844
|
+
const depTask = getTask(dep.id, d);
|
|
8845
|
+
const payload = depTask ? taskEventData(depTask, { unblocked_by: id }) : { id: dep.id, unblocked_by: id, title: dep.title };
|
|
8846
|
+
dispatchWebhook2("task.unblocked", payload, d).catch(() => {});
|
|
8847
|
+
emitLocalEventHooksQuiet({ type: "task.unblocked", payload, databasePath });
|
|
8848
|
+
if (depTask)
|
|
8849
|
+
emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id }, databasePath });
|
|
8850
|
+
}
|
|
8806
8851
|
}
|
|
8807
|
-
return
|
|
8808
|
-
}
|
|
8809
|
-
function getTemplateWithTasks(id, db) {
|
|
8810
|
-
const d = db || getDatabase();
|
|
8811
|
-
const template = getTemplate(id, d);
|
|
8812
|
-
if (!template)
|
|
8813
|
-
return null;
|
|
8814
|
-
const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(template.id);
|
|
8815
|
-
const tasks = rows.map(rowToTemplateTask);
|
|
8816
|
-
return { ...template, tasks };
|
|
8852
|
+
return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: finalVersion, updated_at: timestamp, metadata: meta };
|
|
8817
8853
|
}
|
|
8818
|
-
function
|
|
8854
|
+
function lockTask(id, agentId, db) {
|
|
8819
8855
|
const d = db || getDatabase();
|
|
8820
|
-
const
|
|
8821
|
-
if (!
|
|
8822
|
-
|
|
8823
|
-
|
|
8824
|
-
|
|
8825
|
-
|
|
8826
|
-
|
|
8827
|
-
|
|
8828
|
-
return true;
|
|
8829
|
-
const trimmed = condition.trim();
|
|
8830
|
-
const eqMatch = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
|
|
8831
|
-
if (eqMatch) {
|
|
8832
|
-
const varName = eqMatch[1];
|
|
8833
|
-
const expected = eqMatch[2].trim();
|
|
8834
|
-
return (variables[varName] ?? "") === expected;
|
|
8856
|
+
const task = getTask(id, d);
|
|
8857
|
+
if (!task)
|
|
8858
|
+
throw new TaskNotFoundError(id);
|
|
8859
|
+
if (task.status === "completed" || task.status === "cancelled") {
|
|
8860
|
+
return {
|
|
8861
|
+
success: false,
|
|
8862
|
+
error: `Task is ${task.status} and cannot be locked`
|
|
8863
|
+
};
|
|
8835
8864
|
}
|
|
8836
|
-
|
|
8837
|
-
|
|
8838
|
-
|
|
8839
|
-
|
|
8840
|
-
return
|
|
8865
|
+
if (task.locked_by === agentId && !isLockExpired(task.locked_at)) {
|
|
8866
|
+
const timestamp2 = now();
|
|
8867
|
+
d.run(`UPDATE tasks SET locked_at = ?, updated_at = ?, version = version + 1 WHERE id = ? AND locked_by = ?`, [timestamp2, timestamp2, id, agentId]);
|
|
8868
|
+
logTaskChange(id, "lock_renew", "locked_by", agentId, agentId, agentId, d);
|
|
8869
|
+
return { success: true, locked_by: agentId, locked_at: timestamp2, expires_at: lockExpiresAt(timestamp2) };
|
|
8841
8870
|
}
|
|
8842
|
-
const
|
|
8843
|
-
|
|
8844
|
-
|
|
8845
|
-
|
|
8846
|
-
|
|
8871
|
+
const cutoff = lockExpiryCutoff();
|
|
8872
|
+
const timestamp = now();
|
|
8873
|
+
const result = d.run(`UPDATE tasks SET locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
|
|
8874
|
+
WHERE id = ? AND status NOT IN ('completed', 'cancelled') AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, timestamp, timestamp, id, agentId, cutoff]);
|
|
8875
|
+
if (result.changes === 0) {
|
|
8876
|
+
const current = getTask(id, d);
|
|
8877
|
+
if (!current)
|
|
8878
|
+
throw new TaskNotFoundError(id);
|
|
8879
|
+
if (current.status === "completed" || current.status === "cancelled") {
|
|
8880
|
+
return {
|
|
8881
|
+
success: false,
|
|
8882
|
+
error: `Task is ${current.status} and cannot be locked`
|
|
8883
|
+
};
|
|
8884
|
+
}
|
|
8885
|
+
if (current.locked_by && !isLockExpired(current.locked_at)) {
|
|
8886
|
+
return {
|
|
8887
|
+
success: false,
|
|
8888
|
+
locked_by: current.locked_by,
|
|
8889
|
+
locked_at: current.locked_at,
|
|
8890
|
+
error: `Task is locked by ${current.locked_by}`
|
|
8891
|
+
};
|
|
8892
|
+
}
|
|
8893
|
+
return {
|
|
8894
|
+
success: false,
|
|
8895
|
+
error: `Task ${id} could not be locked because it changed during lock acquisition`
|
|
8896
|
+
};
|
|
8847
8897
|
}
|
|
8848
|
-
|
|
8849
|
-
|
|
8850
|
-
|
|
8851
|
-
|
|
8852
|
-
|
|
8898
|
+
logTaskChange(id, "lock", "locked_by", task.locked_by, agentId, agentId, d);
|
|
8899
|
+
return { success: true, locked_by: agentId, locked_at: timestamp, expires_at: lockExpiresAt(timestamp) };
|
|
8900
|
+
}
|
|
8901
|
+
function unlockTask(id, agentId, db) {
|
|
8902
|
+
const d = db || getDatabase();
|
|
8903
|
+
const task = getTask(id, d);
|
|
8904
|
+
if (!task)
|
|
8905
|
+
throw new TaskNotFoundError(id);
|
|
8906
|
+
if (agentId && task.locked_by && task.locked_by !== agentId) {
|
|
8907
|
+
throw new LockError(id, task.locked_by);
|
|
8853
8908
|
}
|
|
8909
|
+
const timestamp = now();
|
|
8910
|
+
d.run(`UPDATE tasks SET locked_by = NULL, locked_at = NULL, version = version + 1, updated_at = ?
|
|
8911
|
+
WHERE id = ?`, [timestamp, id]);
|
|
8854
8912
|
return true;
|
|
8855
8913
|
}
|
|
8856
|
-
function
|
|
8914
|
+
function getTaskLockStatus(id, db) {
|
|
8857
8915
|
const d = db || getDatabase();
|
|
8858
|
-
const
|
|
8859
|
-
if (!
|
|
8860
|
-
throw new
|
|
8916
|
+
const task = getTask(id, d);
|
|
8917
|
+
if (!task)
|
|
8918
|
+
throw new TaskNotFoundError(id);
|
|
8919
|
+
const expired = isLockExpired(task.locked_at);
|
|
8861
8920
|
return {
|
|
8862
|
-
|
|
8863
|
-
|
|
8864
|
-
|
|
8865
|
-
|
|
8866
|
-
|
|
8867
|
-
|
|
8868
|
-
project_id: template.project_id,
|
|
8869
|
-
plan_id: template.plan_id,
|
|
8870
|
-
metadata: template.metadata,
|
|
8871
|
-
tasks: template.tasks.map((t) => ({
|
|
8872
|
-
position: t.position,
|
|
8873
|
-
title_pattern: t.title_pattern,
|
|
8874
|
-
description: t.description,
|
|
8875
|
-
priority: t.priority,
|
|
8876
|
-
tags: t.tags,
|
|
8877
|
-
task_type: t.task_type,
|
|
8878
|
-
condition: t.condition,
|
|
8879
|
-
include_template_id: t.include_template_id,
|
|
8880
|
-
depends_on_positions: t.depends_on_positions,
|
|
8881
|
-
metadata: t.metadata
|
|
8882
|
-
}))
|
|
8921
|
+
task_id: id,
|
|
8922
|
+
locked: !!task.locked_by && !expired,
|
|
8923
|
+
locked_by: task.locked_by,
|
|
8924
|
+
locked_at: task.locked_at,
|
|
8925
|
+
expires_at: lockExpiresAt(task.locked_at),
|
|
8926
|
+
expired
|
|
8883
8927
|
};
|
|
8884
8928
|
}
|
|
8885
|
-
function
|
|
8886
|
-
const d = db || getDatabase();
|
|
8887
|
-
const taskInputs = (json.tasks || []).map((t) => ({
|
|
8888
|
-
title_pattern: t.title_pattern,
|
|
8889
|
-
description: t.description ?? undefined,
|
|
8890
|
-
priority: t.priority,
|
|
8891
|
-
tags: t.tags,
|
|
8892
|
-
task_type: t.task_type ?? undefined,
|
|
8893
|
-
condition: t.condition ?? undefined,
|
|
8894
|
-
include_template_id: t.include_template_id ?? undefined,
|
|
8895
|
-
depends_on: t.depends_on_positions,
|
|
8896
|
-
metadata: t.metadata
|
|
8897
|
-
}));
|
|
8898
|
-
return createTemplate({
|
|
8899
|
-
name: json.name,
|
|
8900
|
-
title_pattern: json.title_pattern,
|
|
8901
|
-
description: json.description ?? undefined,
|
|
8902
|
-
priority: json.priority,
|
|
8903
|
-
tags: json.tags,
|
|
8904
|
-
variables: json.variables,
|
|
8905
|
-
project_id: json.project_id ?? undefined,
|
|
8906
|
-
plan_id: json.plan_id ?? undefined,
|
|
8907
|
-
metadata: json.metadata,
|
|
8908
|
-
tasks: taskInputs
|
|
8909
|
-
}, d);
|
|
8910
|
-
}
|
|
8911
|
-
function getTemplateVersion(id, version, db) {
|
|
8912
|
-
const d = db || getDatabase();
|
|
8913
|
-
const resolved = resolveTemplateId(id, d);
|
|
8914
|
-
if (!resolved)
|
|
8915
|
-
return null;
|
|
8916
|
-
const row = d.query("SELECT * FROM template_versions WHERE template_id = ? AND version = ?").get(resolved, version);
|
|
8917
|
-
return row || null;
|
|
8918
|
-
}
|
|
8919
|
-
function listTemplateVersions(id, db) {
|
|
8929
|
+
function claimNextTask(agentId, filters, db) {
|
|
8920
8930
|
const d = db || getDatabase();
|
|
8921
|
-
const
|
|
8922
|
-
|
|
8923
|
-
|
|
8924
|
-
|
|
8931
|
+
const MAX_ATTEMPTS = 25;
|
|
8932
|
+
const tried = new Set;
|
|
8933
|
+
for (let attempt = 0;attempt < MAX_ATTEMPTS; attempt++) {
|
|
8934
|
+
const outcome = d.transaction(() => {
|
|
8935
|
+
const task = getNextTask(agentId, filters, d);
|
|
8936
|
+
if (!task)
|
|
8937
|
+
return { done: true, task: null };
|
|
8938
|
+
if (tried.has(task.id))
|
|
8939
|
+
return { done: true, task: null };
|
|
8940
|
+
tried.add(task.id);
|
|
8941
|
+
try {
|
|
8942
|
+
return { done: true, task: startTask(task.id, agentId, d) };
|
|
8943
|
+
} catch {
|
|
8944
|
+
return { done: false, task: null };
|
|
8945
|
+
}
|
|
8946
|
+
})();
|
|
8947
|
+
if (outcome.done)
|
|
8948
|
+
return outcome.task;
|
|
8949
|
+
}
|
|
8950
|
+
return null;
|
|
8925
8951
|
}
|
|
8926
|
-
function
|
|
8927
|
-
const
|
|
8928
|
-
|
|
8929
|
-
|
|
8930
|
-
|
|
8931
|
-
|
|
8952
|
+
function getNextTask(agentId, filters, db) {
|
|
8953
|
+
const d = db || getDatabase();
|
|
8954
|
+
clearExpiredLocks(d);
|
|
8955
|
+
const conditions = ["status = 'pending'", "(locked_by IS NULL OR locked_at < ?)"];
|
|
8956
|
+
const params = [lockExpiryCutoff()];
|
|
8957
|
+
if (filters?.project_id) {
|
|
8958
|
+
conditions.push("project_id = ?");
|
|
8959
|
+
params.push(filters.project_id);
|
|
8932
8960
|
}
|
|
8933
|
-
|
|
8934
|
-
|
|
8935
|
-
|
|
8936
|
-
missing.push(v.name);
|
|
8937
|
-
}
|
|
8961
|
+
if (filters?.task_list_id) {
|
|
8962
|
+
conditions.push("task_list_id = ?");
|
|
8963
|
+
params.push(filters.task_list_id);
|
|
8938
8964
|
}
|
|
8939
|
-
if (
|
|
8940
|
-
|
|
8965
|
+
if (filters?.plan_id) {
|
|
8966
|
+
conditions.push("plan_id = ?");
|
|
8967
|
+
params.push(filters.plan_id);
|
|
8941
8968
|
}
|
|
8942
|
-
|
|
8969
|
+
if (filters?.tags && filters.tags.length > 0) {
|
|
8970
|
+
const placeholders = filters.tags.map(() => "?").join(",");
|
|
8971
|
+
conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
|
|
8972
|
+
params.push(...filters.tags);
|
|
8973
|
+
}
|
|
8974
|
+
conditions.push("id NOT IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status != 'completed')");
|
|
8975
|
+
const where = conditions.join(" AND ");
|
|
8976
|
+
let recentProjectIds = [];
|
|
8977
|
+
if (agentId) {
|
|
8978
|
+
const recentRows = d.query(`SELECT DISTINCT project_id FROM tasks WHERE assigned_to = ? AND status = 'completed' AND project_id IS NOT NULL ORDER BY completed_at DESC LIMIT 3`).all(agentId);
|
|
8979
|
+
recentProjectIds = recentRows.map((r) => r.project_id);
|
|
8980
|
+
}
|
|
8981
|
+
let sql = `SELECT * FROM tasks WHERE ${where} ORDER BY `;
|
|
8982
|
+
if (agentId) {
|
|
8983
|
+
sql += `CASE WHEN assigned_to = ? THEN 0 WHEN assigned_to IS NULL THEN 1 ELSE 2 END, `;
|
|
8984
|
+
params.push(agentId);
|
|
8985
|
+
}
|
|
8986
|
+
if (recentProjectIds.length > 0) {
|
|
8987
|
+
const placeholders = recentProjectIds.map(() => "?").join(",");
|
|
8988
|
+
sql += `CASE WHEN project_id IN (${placeholders}) THEN 0 ELSE 1 END, `;
|
|
8989
|
+
params.push(...recentProjectIds);
|
|
8990
|
+
}
|
|
8991
|
+
sql += `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END, created_at ASC LIMIT 1`;
|
|
8992
|
+
const row = d.query(sql).get(...params);
|
|
8993
|
+
return row ? rowToTask(row) : null;
|
|
8943
8994
|
}
|
|
8944
|
-
function
|
|
8945
|
-
|
|
8946
|
-
|
|
8947
|
-
|
|
8995
|
+
function getActiveWork(filters, db) {
|
|
8996
|
+
const d = db || getDatabase();
|
|
8997
|
+
clearExpiredLocks(d);
|
|
8998
|
+
const conditions = ["status = 'in_progress'"];
|
|
8999
|
+
const params = [];
|
|
9000
|
+
if (filters?.project_id) {
|
|
9001
|
+
conditions.push("project_id = ?");
|
|
9002
|
+
params.push(filters.project_id);
|
|
8948
9003
|
}
|
|
8949
|
-
|
|
9004
|
+
if (filters?.task_list_id) {
|
|
9005
|
+
conditions.push("task_list_id = ?");
|
|
9006
|
+
params.push(filters.task_list_id);
|
|
9007
|
+
}
|
|
9008
|
+
const where = conditions.join(" AND ");
|
|
9009
|
+
const rows = d.query(`SELECT id, short_id, title, priority, assigned_to, locked_by, locked_at, updated_at FROM tasks WHERE ${where} ORDER BY
|
|
9010
|
+
CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END,
|
|
9011
|
+
updated_at DESC`).all(...params);
|
|
9012
|
+
return rows;
|
|
8950
9013
|
}
|
|
8951
|
-
function
|
|
9014
|
+
function getTasksChangedSince(since, filters, db) {
|
|
8952
9015
|
const d = db || getDatabase();
|
|
8953
|
-
const
|
|
8954
|
-
|
|
8955
|
-
|
|
8956
|
-
|
|
8957
|
-
|
|
8958
|
-
throw new Error(`Circular template reference detected: ${template.id}`);
|
|
9016
|
+
const conditions = ["updated_at > ?"];
|
|
9017
|
+
const params = [since];
|
|
9018
|
+
if (filters?.project_id) {
|
|
9019
|
+
conditions.push("project_id = ?");
|
|
9020
|
+
params.push(filters.project_id);
|
|
8959
9021
|
}
|
|
8960
|
-
|
|
8961
|
-
|
|
8962
|
-
|
|
8963
|
-
const input = taskFromTemplate(templateId, { project_id: projectId, task_list_id: taskListId }, d);
|
|
8964
|
-
const task = createTask(input, d);
|
|
8965
|
-
return [task];
|
|
9022
|
+
if (filters?.task_list_id) {
|
|
9023
|
+
conditions.push("task_list_id = ?");
|
|
9024
|
+
params.push(filters.task_list_id);
|
|
8966
9025
|
}
|
|
8967
|
-
const
|
|
8968
|
-
const
|
|
8969
|
-
|
|
8970
|
-
|
|
8971
|
-
|
|
8972
|
-
|
|
8973
|
-
|
|
8974
|
-
|
|
8975
|
-
|
|
8976
|
-
|
|
8977
|
-
|
|
8978
|
-
|
|
8979
|
-
|
|
9026
|
+
const where = conditions.join(" AND ");
|
|
9027
|
+
const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at DESC`).all(...params);
|
|
9028
|
+
return rows.map(rowToTask);
|
|
9029
|
+
}
|
|
9030
|
+
function failTask(id, agentId, reason, options, db) {
|
|
9031
|
+
const d = db || getDatabase();
|
|
9032
|
+
const databasePath = databasePathFromDatabase(d);
|
|
9033
|
+
const task = getTask(id, d);
|
|
9034
|
+
if (!task)
|
|
9035
|
+
throw new TaskNotFoundError(id);
|
|
9036
|
+
const meta = {
|
|
9037
|
+
...task.metadata,
|
|
9038
|
+
_failure: {
|
|
9039
|
+
reason: reason || "Unknown failure",
|
|
9040
|
+
error_code: options?.error_code || null,
|
|
9041
|
+
failed_by: agentId || null,
|
|
9042
|
+
failed_at: now(),
|
|
9043
|
+
retry_requested: options?.retry || false
|
|
8980
9044
|
}
|
|
8981
|
-
|
|
8982
|
-
|
|
8983
|
-
|
|
9045
|
+
};
|
|
9046
|
+
const timestamp = now();
|
|
9047
|
+
const failTx = d.transaction(() => {
|
|
9048
|
+
const res = d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
|
|
9049
|
+
WHERE id = ? AND version = ?`, [JSON.stringify(meta), timestamp, id, task.version]);
|
|
9050
|
+
if (res.changes === 0) {
|
|
9051
|
+
const current = getTask(id, d);
|
|
9052
|
+
throw new VersionConflictError(id, task.version, current?.version ?? -1);
|
|
8984
9053
|
}
|
|
8985
|
-
|
|
8986
|
-
|
|
8987
|
-
|
|
8988
|
-
|
|
8989
|
-
|
|
8990
|
-
|
|
8991
|
-
|
|
8992
|
-
|
|
8993
|
-
|
|
8994
|
-
|
|
8995
|
-
|
|
8996
|
-
|
|
8997
|
-
|
|
8998
|
-
|
|
8999
|
-
|
|
9000
|
-
|
|
9001
|
-
|
|
9002
|
-
|
|
9003
|
-
|
|
9004
|
-
|
|
9005
|
-
|
|
9006
|
-
|
|
9007
|
-
|
|
9008
|
-
|
|
9009
|
-
|
|
9010
|
-
|
|
9011
|
-
|
|
9012
|
-
const
|
|
9013
|
-
|
|
9014
|
-
if (
|
|
9015
|
-
|
|
9054
|
+
});
|
|
9055
|
+
failTx();
|
|
9056
|
+
const failedTask = {
|
|
9057
|
+
...task,
|
|
9058
|
+
status: "failed",
|
|
9059
|
+
locked_by: null,
|
|
9060
|
+
locked_at: null,
|
|
9061
|
+
metadata: meta,
|
|
9062
|
+
version: task.version + 1,
|
|
9063
|
+
updated_at: timestamp
|
|
9064
|
+
};
|
|
9065
|
+
logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
|
|
9066
|
+
const failurePayload = taskEventData(failedTask, { reason, error_code: options?.error_code, agent_id: agentId });
|
|
9067
|
+
dispatchWebhook2("task.failed", failurePayload, d).catch(() => {});
|
|
9068
|
+
emitLocalEventHooksQuiet({ type: "task.failed", payload: failurePayload, databasePath });
|
|
9069
|
+
emitSharedTaskEventQuiet({ type: "task.failed", task: failedTask, data: { reason, error_code: options?.error_code, agent_id: agentId }, severity: "warning", databasePath });
|
|
9070
|
+
let retryTask;
|
|
9071
|
+
if (options?.retry) {
|
|
9072
|
+
const retryCount = (task.retry_count || 0) + 1;
|
|
9073
|
+
const maxRetries = task.max_retries || 3;
|
|
9074
|
+
if (retryCount > maxRetries) {
|
|
9075
|
+
d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [
|
|
9076
|
+
JSON.stringify({ ...meta, _retry_exhausted: { retry_count: retryCount - 1, max_retries: maxRetries } }),
|
|
9077
|
+
id
|
|
9078
|
+
]);
|
|
9079
|
+
} else {
|
|
9080
|
+
const backoffMinutes = Math.pow(5, retryCount - 1);
|
|
9081
|
+
const retryAfter = options.retry_after || new Date(Date.now() + backoffMinutes * 60 * 1000).toISOString();
|
|
9082
|
+
let title = task.title;
|
|
9083
|
+
if (task.short_id && title.startsWith(task.short_id + ": ")) {
|
|
9084
|
+
title = title.slice(task.short_id.length + 2);
|
|
9016
9085
|
}
|
|
9086
|
+
retryTask = createTask({
|
|
9087
|
+
title,
|
|
9088
|
+
description: task.description ?? undefined,
|
|
9089
|
+
priority: task.priority,
|
|
9090
|
+
project_id: task.project_id ?? undefined,
|
|
9091
|
+
task_list_id: task.task_list_id ?? undefined,
|
|
9092
|
+
plan_id: task.plan_id ?? undefined,
|
|
9093
|
+
assigned_to: task.assigned_to ?? undefined,
|
|
9094
|
+
tags: task.tags,
|
|
9095
|
+
metadata: { ...task.metadata, _retry: { original_id: task.id, retry_count: retryCount, max_retries: maxRetries, retry_after: retryAfter, failure_reason: reason } },
|
|
9096
|
+
estimated_minutes: task.estimated_minutes ?? undefined,
|
|
9097
|
+
recurrence_rule: task.recurrence_rule ?? undefined,
|
|
9098
|
+
due_at: retryAfter
|
|
9099
|
+
}, d);
|
|
9100
|
+
d.run("UPDATE tasks SET retry_count = ?, max_retries = ?, retry_after = ? WHERE id = ?", [retryCount, maxRetries, retryAfter, retryTask.id]);
|
|
9017
9101
|
}
|
|
9018
9102
|
}
|
|
9019
|
-
return
|
|
9103
|
+
return { task: failedTask, retryTask };
|
|
9020
9104
|
}
|
|
9021
|
-
function
|
|
9105
|
+
function getStaleTasks(staleQuery = 30, filters, db) {
|
|
9022
9106
|
const d = db || getDatabase();
|
|
9023
|
-
const
|
|
9024
|
-
|
|
9025
|
-
|
|
9026
|
-
const
|
|
9027
|
-
|
|
9028
|
-
|
|
9029
|
-
|
|
9030
|
-
|
|
9031
|
-
|
|
9032
|
-
|
|
9033
|
-
|
|
9034
|
-
|
|
9035
|
-
|
|
9036
|
-
|
|
9037
|
-
|
|
9038
|
-
}
|
|
9039
|
-
|
|
9040
|
-
|
|
9041
|
-
|
|
9042
|
-
|
|
9043
|
-
|
|
9044
|
-
|
|
9045
|
-
|
|
9046
|
-
|
|
9047
|
-
|
|
9048
|
-
|
|
9049
|
-
|
|
9050
|
-
|
|
9107
|
+
const staleMinutes = typeof staleQuery === "number" ? staleQuery : staleQuery.minutes ?? (staleQuery.hours !== undefined ? staleQuery.hours * 60 : 30);
|
|
9108
|
+
const effectiveFilters = typeof staleQuery === "number" ? filters : { project_id: staleQuery.project_id, task_list_id: staleQuery.task_list_id };
|
|
9109
|
+
const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
|
|
9110
|
+
const conditions = [
|
|
9111
|
+
"status = 'in_progress'",
|
|
9112
|
+
"(updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))"
|
|
9113
|
+
];
|
|
9114
|
+
const params = [cutoff, cutoff];
|
|
9115
|
+
if (effectiveFilters?.project_id) {
|
|
9116
|
+
conditions.push("project_id = ?");
|
|
9117
|
+
params.push(effectiveFilters.project_id);
|
|
9118
|
+
}
|
|
9119
|
+
if (effectiveFilters?.task_list_id) {
|
|
9120
|
+
conditions.push("task_list_id = ?");
|
|
9121
|
+
params.push(effectiveFilters.task_list_id);
|
|
9122
|
+
}
|
|
9123
|
+
const where = conditions.join(" AND ");
|
|
9124
|
+
const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at ASC`).all(...params);
|
|
9125
|
+
return rows.map(rowToTask);
|
|
9126
|
+
}
|
|
9127
|
+
function stealTask(agentId, opts, db) {
|
|
9128
|
+
const d = db || getDatabase();
|
|
9129
|
+
const databasePath = databasePathFromDatabase(d);
|
|
9130
|
+
const staleMinutes = opts?.stale_minutes ?? 30;
|
|
9131
|
+
const staleTasks = getStaleTasks(staleMinutes, { project_id: opts?.project_id, task_list_id: opts?.task_list_id }, d);
|
|
9132
|
+
if (staleTasks.length === 0)
|
|
9133
|
+
return null;
|
|
9134
|
+
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
9135
|
+
staleTasks.sort((a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9));
|
|
9136
|
+
const target = staleTasks[0];
|
|
9137
|
+
const timestamp = now();
|
|
9138
|
+
const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
|
|
9139
|
+
const result = d.run(`UPDATE tasks SET assigned_to = ?, locked_by = ?, locked_at = ?, updated_at = ?, version = version + 1
|
|
9140
|
+
WHERE id = ? AND status = 'in_progress' AND (updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))`, [agentId, agentId, timestamp, timestamp, target.id, cutoff, cutoff]);
|
|
9141
|
+
if (result.changes === 0)
|
|
9142
|
+
return null;
|
|
9143
|
+
logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
|
|
9144
|
+
logTaskChange(target.id, "steal", "locked_by", target.locked_by, agentId, agentId, d);
|
|
9145
|
+
const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
|
|
9146
|
+
const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
|
|
9147
|
+
dispatchWebhook2("task.assigned", payload, d).catch(() => {});
|
|
9148
|
+
emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
|
|
9149
|
+
emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to }, databasePath });
|
|
9150
|
+
return stolenTask;
|
|
9151
|
+
}
|
|
9152
|
+
function claimOrSteal(agentId, filters, db) {
|
|
9153
|
+
const d = db || getDatabase();
|
|
9154
|
+
const tx = d.transaction(() => {
|
|
9155
|
+
const next = getNextTask(agentId, filters, d);
|
|
9156
|
+
if (next) {
|
|
9157
|
+
const started = startTask(next.id, agentId, d);
|
|
9158
|
+
return { task: started, stolen: false };
|
|
9051
9159
|
}
|
|
9160
|
+
const stolen = stealTask(agentId, { stale_minutes: filters?.stale_minutes, project_id: filters?.project_id, task_list_id: filters?.task_list_id }, d);
|
|
9161
|
+
if (stolen)
|
|
9162
|
+
return { task: stolen, stolen: true };
|
|
9163
|
+
return null;
|
|
9164
|
+
});
|
|
9165
|
+
return tx();
|
|
9166
|
+
}
|
|
9167
|
+
function spawnNextRecurrence(completedTask, db, completedAt) {
|
|
9168
|
+
const recurrenceBase = completedTask.due_at ? new Date(completedTask.due_at) : new Date(completedAt);
|
|
9169
|
+
const dueAt = nextOccurrence(completedTask.recurrence_rule, recurrenceBase);
|
|
9170
|
+
let title = completedTask.title;
|
|
9171
|
+
if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
|
|
9172
|
+
title = title.slice(completedTask.short_id.length + 2);
|
|
9052
9173
|
}
|
|
9053
|
-
|
|
9054
|
-
|
|
9055
|
-
|
|
9056
|
-
description:
|
|
9057
|
-
|
|
9058
|
-
|
|
9059
|
-
|
|
9060
|
-
|
|
9174
|
+
const recurrenceParentId = completedTask.recurrence_parent_id || completedTask.id;
|
|
9175
|
+
return createTask({
|
|
9176
|
+
title,
|
|
9177
|
+
description: completedTask.description ?? undefined,
|
|
9178
|
+
priority: completedTask.priority,
|
|
9179
|
+
project_id: completedTask.project_id ?? undefined,
|
|
9180
|
+
task_list_id: completedTask.task_list_id ?? undefined,
|
|
9181
|
+
plan_id: completedTask.plan_id ?? undefined,
|
|
9182
|
+
assigned_to: completedTask.assigned_to ?? undefined,
|
|
9183
|
+
tags: completedTask.tags,
|
|
9184
|
+
metadata: completedTask.metadata,
|
|
9185
|
+
estimated_minutes: completedTask.estimated_minutes ?? undefined,
|
|
9186
|
+
sla_minutes: completedTask.sla_minutes ?? undefined,
|
|
9187
|
+
recurrence_rule: completedTask.recurrence_rule,
|
|
9188
|
+
recurrence_parent_id: recurrenceParentId,
|
|
9189
|
+
due_at: dueAt
|
|
9190
|
+
}, db);
|
|
9061
9191
|
}
|
|
9062
|
-
var
|
|
9192
|
+
var MAX_SPAWN_DEPTH = 10;
|
|
9193
|
+
var init_task_lifecycle = __esm(() => {
|
|
9194
|
+
init_types();
|
|
9063
9195
|
init_database();
|
|
9064
|
-
|
|
9065
|
-
|
|
9196
|
+
init_completion_guard();
|
|
9197
|
+
init_event_emission_safety();
|
|
9198
|
+
init_event_hooks();
|
|
9199
|
+
init_shared_events();
|
|
9200
|
+
init_audit();
|
|
9201
|
+
init_recurrence();
|
|
9202
|
+
init_webhooks();
|
|
9203
|
+
init_templates();
|
|
9204
|
+
init_task_crud();
|
|
9205
|
+
init_task_graph();
|
|
9066
9206
|
});
|
|
9067
9207
|
|
|
9068
|
-
// src/db/task-
|
|
9069
|
-
function
|
|
9070
|
-
|
|
9071
|
-
|
|
9072
|
-
|
|
9073
|
-
|
|
9074
|
-
|
|
9075
|
-
|
|
9076
|
-
|
|
9208
|
+
// src/db/task-crud.ts
|
|
9209
|
+
function rowToTask(row) {
|
|
9210
|
+
return {
|
|
9211
|
+
...row,
|
|
9212
|
+
tags: JSON.parse(row.tags || "[]"),
|
|
9213
|
+
metadata: JSON.parse(row.metadata || "{}"),
|
|
9214
|
+
status: row.status,
|
|
9215
|
+
priority: row.priority,
|
|
9216
|
+
requires_approval: !!row.requires_approval
|
|
9217
|
+
};
|
|
9218
|
+
}
|
|
9219
|
+
function insertTaskTags(taskId, tags, db) {
|
|
9220
|
+
if (tags.length === 0)
|
|
9221
|
+
return;
|
|
9222
|
+
const stmt = db.prepare("INSERT OR IGNORE INTO task_tags (task_id, tag) VALUES (?, ?)");
|
|
9223
|
+
for (const tag of tags) {
|
|
9224
|
+
if (tag)
|
|
9225
|
+
stmt.run(taskId, tag);
|
|
9077
9226
|
}
|
|
9078
|
-
d.run("INSERT OR IGNORE INTO task_dependencies (task_id, depends_on) VALUES (?, ?)", [taskId, dependsOn]);
|
|
9079
9227
|
}
|
|
9080
|
-
function
|
|
9081
|
-
|
|
9082
|
-
|
|
9083
|
-
return result.changes > 0;
|
|
9228
|
+
function replaceTaskTags(taskId, tags, db) {
|
|
9229
|
+
db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
|
|
9230
|
+
insertTaskTags(taskId, tags, db);
|
|
9084
9231
|
}
|
|
9085
|
-
function
|
|
9232
|
+
function addMetadataConditions(metadata, conditions, params) {
|
|
9233
|
+
if (!metadata)
|
|
9234
|
+
return;
|
|
9235
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
9236
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(key)) {
|
|
9237
|
+
throw new Error(`Invalid metadata filter key: ${key}`);
|
|
9238
|
+
}
|
|
9239
|
+
conditions.push(`json_extract(metadata, '$."${key}"') = ?`);
|
|
9240
|
+
params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
|
|
9241
|
+
}
|
|
9242
|
+
}
|
|
9243
|
+
function createTask(input, db) {
|
|
9086
9244
|
const d = db || getDatabase();
|
|
9087
|
-
|
|
9245
|
+
const timestamp = now();
|
|
9246
|
+
const tags = input.tags || [];
|
|
9247
|
+
const machineId = currentStorageMachineId(d);
|
|
9248
|
+
const assignedBy = input.assigned_by || input.agent_id;
|
|
9249
|
+
const assignedFromProject = input.assigned_from_project || null;
|
|
9250
|
+
let id = uuid();
|
|
9251
|
+
for (let attempt = 0;attempt < 3; attempt++) {
|
|
9252
|
+
try {
|
|
9253
|
+
d.run(`INSERT INTO tasks (id, short_id, project_id, parent_id, plan_id, task_list_id, cycle_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at, due_at, estimated_minutes, sla_minutes, confidence, retry_count, max_retries, retry_after, requires_approval, approved_by, approved_at, recurrence_rule, recurrence_parent_id, spawns_template_id, reason, spawned_from_session, assigned_by, assigned_from_project, task_type, machine_id)
|
|
9254
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
9255
|
+
id,
|
|
9256
|
+
null,
|
|
9257
|
+
input.project_id || null,
|
|
9258
|
+
input.parent_id || null,
|
|
9259
|
+
input.plan_id || null,
|
|
9260
|
+
input.task_list_id || null,
|
|
9261
|
+
input.cycle_id || null,
|
|
9262
|
+
input.title,
|
|
9263
|
+
input.description || null,
|
|
9264
|
+
input.status || "pending",
|
|
9265
|
+
input.priority || "medium",
|
|
9266
|
+
input.agent_id || null,
|
|
9267
|
+
input.assigned_to || null,
|
|
9268
|
+
input.session_id || null,
|
|
9269
|
+
input.working_dir || null,
|
|
9270
|
+
JSON.stringify(tags),
|
|
9271
|
+
JSON.stringify(input.metadata || {}),
|
|
9272
|
+
timestamp,
|
|
9273
|
+
timestamp,
|
|
9274
|
+
input.due_at || null,
|
|
9275
|
+
input.estimated_minutes || null,
|
|
9276
|
+
input.sla_minutes ?? null,
|
|
9277
|
+
input.confidence ?? null,
|
|
9278
|
+
input.retry_count ?? 0,
|
|
9279
|
+
input.max_retries ?? 3,
|
|
9280
|
+
input.retry_after ?? null,
|
|
9281
|
+
input.requires_approval ? 1 : 0,
|
|
9282
|
+
null,
|
|
9283
|
+
null,
|
|
9284
|
+
input.recurrence_rule || null,
|
|
9285
|
+
input.recurrence_parent_id || null,
|
|
9286
|
+
input.spawns_template_id || null,
|
|
9287
|
+
input.reason || null,
|
|
9288
|
+
input.spawned_from_session || null,
|
|
9289
|
+
assignedBy || null,
|
|
9290
|
+
assignedFromProject || null,
|
|
9291
|
+
input.task_type || null,
|
|
9292
|
+
machineId
|
|
9293
|
+
]);
|
|
9294
|
+
break;
|
|
9295
|
+
} catch (e) {
|
|
9296
|
+
if (attempt < 2 && e?.message?.includes("UNIQUE constraint failed: tasks.id")) {
|
|
9297
|
+
id = uuid();
|
|
9298
|
+
continue;
|
|
9299
|
+
}
|
|
9300
|
+
throw e;
|
|
9301
|
+
}
|
|
9302
|
+
}
|
|
9303
|
+
if (tags.length > 0) {
|
|
9304
|
+
insertTaskTags(id, tags, d);
|
|
9305
|
+
}
|
|
9306
|
+
const task = getTask(id, d);
|
|
9307
|
+
const payload = taskEventData(task);
|
|
9308
|
+
const databasePath = databasePathFromDatabase(d);
|
|
9309
|
+
dispatchWebhook2("task.created", payload, d).catch(() => {});
|
|
9310
|
+
emitLocalEventHooksQuiet({ type: "task.created", payload, databasePath });
|
|
9311
|
+
emitSharedTaskEventQuiet({ type: "task.created", task, databasePath });
|
|
9312
|
+
return task;
|
|
9088
9313
|
}
|
|
9089
|
-
function
|
|
9314
|
+
function getTask(id, db) {
|
|
9090
9315
|
const d = db || getDatabase();
|
|
9091
|
-
|
|
9316
|
+
const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
|
|
9317
|
+
if (!row)
|
|
9318
|
+
return null;
|
|
9319
|
+
return rowToTask(row);
|
|
9092
9320
|
}
|
|
9093
|
-
function
|
|
9321
|
+
function getTaskWithRelations(id, db) {
|
|
9094
9322
|
const d = db || getDatabase();
|
|
9095
|
-
const
|
|
9096
|
-
if (!
|
|
9097
|
-
|
|
9098
|
-
const
|
|
9099
|
-
|
|
9100
|
-
|
|
9101
|
-
|
|
9102
|
-
|
|
9103
|
-
|
|
9104
|
-
|
|
9105
|
-
|
|
9106
|
-
|
|
9107
|
-
|
|
9108
|
-
|
|
9109
|
-
|
|
9110
|
-
|
|
9111
|
-
|
|
9112
|
-
|
|
9323
|
+
const task = getTask(id, d);
|
|
9324
|
+
if (!task)
|
|
9325
|
+
return null;
|
|
9326
|
+
const subtaskRows = d.query("SELECT * FROM tasks WHERE parent_id = ? ORDER BY created_at").all(id);
|
|
9327
|
+
const subtasks = subtaskRows.map(rowToTask);
|
|
9328
|
+
const depRows = d.query(`SELECT t.* FROM tasks t
|
|
9329
|
+
JOIN task_dependencies td ON td.depends_on = t.id
|
|
9330
|
+
WHERE td.task_id = ?`).all(id);
|
|
9331
|
+
const dependencies = depRows.map(rowToTask);
|
|
9332
|
+
const blockedByRows = d.query(`SELECT t.* FROM tasks t
|
|
9333
|
+
JOIN task_dependencies td ON td.task_id = t.id
|
|
9334
|
+
WHERE td.depends_on = ?`).all(id);
|
|
9335
|
+
const blocked_by = blockedByRows.map(rowToTask);
|
|
9336
|
+
const comments = d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(id);
|
|
9337
|
+
const parent = task.parent_id ? getTask(task.parent_id, d) : null;
|
|
9338
|
+
const checklist = getChecklist(id, d);
|
|
9339
|
+
return {
|
|
9340
|
+
...task,
|
|
9341
|
+
subtasks,
|
|
9342
|
+
dependencies,
|
|
9343
|
+
blocked_by,
|
|
9344
|
+
comments,
|
|
9345
|
+
parent,
|
|
9346
|
+
checklist
|
|
9113
9347
|
};
|
|
9114
|
-
return createTask(input, d);
|
|
9115
9348
|
}
|
|
9116
|
-
function
|
|
9349
|
+
function listTasks(filter = {}, db) {
|
|
9117
9350
|
const d = db || getDatabase();
|
|
9118
|
-
const
|
|
9119
|
-
|
|
9120
|
-
|
|
9121
|
-
|
|
9122
|
-
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
return depTask && depTask.status !== "completed";
|
|
9126
|
-
});
|
|
9127
|
-
return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
|
|
9351
|
+
const { clearExpiredLocks: clearExpiredLocks2 } = (init_database(), __toCommonJS(exports_database));
|
|
9352
|
+
clearExpiredLocks2(d);
|
|
9353
|
+
const conditions = [];
|
|
9354
|
+
const params = [];
|
|
9355
|
+
if (filter.project_id) {
|
|
9356
|
+
conditions.push("project_id = ?");
|
|
9357
|
+
params.push(filter.project_id);
|
|
9128
9358
|
}
|
|
9129
|
-
|
|
9130
|
-
|
|
9131
|
-
|
|
9132
|
-
visited.add(id);
|
|
9133
|
-
const deps = d.query("SELECT depends_on FROM task_dependencies WHERE task_id = ?").all(id);
|
|
9134
|
-
return deps.map((dep) => {
|
|
9135
|
-
const depTask = getTask(dep.depends_on, d);
|
|
9136
|
-
if (!depTask)
|
|
9137
|
-
return null;
|
|
9138
|
-
return { task: toNode(depTask), depends_on: buildUp(dep.depends_on, visited), blocks: [] };
|
|
9139
|
-
}).filter(Boolean);
|
|
9359
|
+
if (filter.ids && filter.ids.length > 0) {
|
|
9360
|
+
conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
|
|
9361
|
+
params.push(...filter.ids);
|
|
9140
9362
|
}
|
|
9141
|
-
|
|
9142
|
-
if (
|
|
9143
|
-
|
|
9144
|
-
|
|
9145
|
-
|
|
9146
|
-
|
|
9147
|
-
|
|
9148
|
-
if (!depTask)
|
|
9149
|
-
return null;
|
|
9150
|
-
return { task: toNode(depTask), depends_on: [], blocks: buildDown(dep.task_id, visited) };
|
|
9151
|
-
}).filter(Boolean);
|
|
9363
|
+
if (filter.parent_id !== undefined) {
|
|
9364
|
+
if (filter.parent_id === null) {
|
|
9365
|
+
conditions.push("parent_id IS NULL");
|
|
9366
|
+
} else {
|
|
9367
|
+
conditions.push("parent_id = ?");
|
|
9368
|
+
params.push(filter.parent_id);
|
|
9369
|
+
}
|
|
9152
9370
|
}
|
|
9153
|
-
|
|
9154
|
-
|
|
9155
|
-
|
|
9156
|
-
|
|
9157
|
-
}
|
|
9158
|
-
|
|
9159
|
-
|
|
9160
|
-
|
|
9161
|
-
if (!task)
|
|
9162
|
-
throw new TaskNotFoundError(taskId);
|
|
9163
|
-
const sets = ["updated_at = ?", "version = version + 1"];
|
|
9164
|
-
const params = [now()];
|
|
9165
|
-
if (target.task_list_id !== undefined) {
|
|
9166
|
-
sets.push("task_list_id = ?");
|
|
9167
|
-
params.push(target.task_list_id);
|
|
9371
|
+
if (filter.status) {
|
|
9372
|
+
if (Array.isArray(filter.status)) {
|
|
9373
|
+
conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
|
|
9374
|
+
params.push(...filter.status);
|
|
9375
|
+
} else {
|
|
9376
|
+
conditions.push("status = ?");
|
|
9377
|
+
params.push(filter.status);
|
|
9378
|
+
}
|
|
9168
9379
|
}
|
|
9169
|
-
if (
|
|
9170
|
-
|
|
9171
|
-
|
|
9380
|
+
if (filter.priority) {
|
|
9381
|
+
if (Array.isArray(filter.priority)) {
|
|
9382
|
+
conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
|
|
9383
|
+
params.push(...filter.priority);
|
|
9384
|
+
} else {
|
|
9385
|
+
conditions.push("priority = ?");
|
|
9386
|
+
params.push(filter.priority);
|
|
9387
|
+
}
|
|
9172
9388
|
}
|
|
9173
|
-
if (
|
|
9174
|
-
|
|
9175
|
-
params.push(
|
|
9389
|
+
if (filter.assigned_to) {
|
|
9390
|
+
conditions.push("assigned_to = ?");
|
|
9391
|
+
params.push(filter.assigned_to);
|
|
9176
9392
|
}
|
|
9177
|
-
|
|
9178
|
-
|
|
9179
|
-
|
|
9180
|
-
}
|
|
9181
|
-
|
|
9182
|
-
|
|
9183
|
-
|
|
9184
|
-
|
|
9185
|
-
|
|
9186
|
-
|
|
9187
|
-
|
|
9188
|
-
|
|
9189
|
-
|
|
9190
|
-
|
|
9191
|
-
|
|
9192
|
-
|
|
9193
|
-
|
|
9393
|
+
if (filter.agent_id) {
|
|
9394
|
+
conditions.push("agent_id = ?");
|
|
9395
|
+
params.push(filter.agent_id);
|
|
9396
|
+
}
|
|
9397
|
+
if (filter.session_id) {
|
|
9398
|
+
conditions.push("session_id = ?");
|
|
9399
|
+
params.push(filter.session_id);
|
|
9400
|
+
}
|
|
9401
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
9402
|
+
const placeholders = filter.tags.map(() => "?").join(",");
|
|
9403
|
+
conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
|
|
9404
|
+
params.push(...filter.tags);
|
|
9405
|
+
}
|
|
9406
|
+
if (filter.plan_id) {
|
|
9407
|
+
conditions.push("plan_id = ?");
|
|
9408
|
+
params.push(filter.plan_id);
|
|
9409
|
+
}
|
|
9410
|
+
if (filter.task_list_id) {
|
|
9411
|
+
conditions.push("task_list_id = ?");
|
|
9412
|
+
params.push(filter.task_list_id);
|
|
9413
|
+
}
|
|
9414
|
+
if (filter.has_recurrence === true) {
|
|
9415
|
+
conditions.push("recurrence_rule IS NOT NULL");
|
|
9416
|
+
} else if (filter.has_recurrence === false) {
|
|
9417
|
+
conditions.push("recurrence_rule IS NULL");
|
|
9418
|
+
}
|
|
9419
|
+
if (filter.task_type) {
|
|
9420
|
+
if (Array.isArray(filter.task_type)) {
|
|
9421
|
+
conditions.push(`task_type IN (${filter.task_type.map(() => "?").join(",")})`);
|
|
9422
|
+
params.push(...filter.task_type);
|
|
9423
|
+
} else {
|
|
9424
|
+
conditions.push("task_type = ?");
|
|
9425
|
+
params.push(filter.task_type);
|
|
9194
9426
|
}
|
|
9195
9427
|
}
|
|
9196
|
-
|
|
9428
|
+
addMetadataConditions(filter.metadata, conditions, params);
|
|
9429
|
+
const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
|
|
9430
|
+
if (filter.cursor) {
|
|
9431
|
+
try {
|
|
9432
|
+
const decoded = JSON.parse(Buffer.from(filter.cursor, "base64").toString("utf8"));
|
|
9433
|
+
conditions.push(`(${PRIORITY_RANK} > ? OR (${PRIORITY_RANK} = ? AND created_at < ?) OR (${PRIORITY_RANK} = ? AND created_at = ? AND id > ?))`);
|
|
9434
|
+
params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
|
|
9435
|
+
} catch {}
|
|
9436
|
+
}
|
|
9437
|
+
if (!filter.include_archived) {
|
|
9438
|
+
conditions.push("archived_at IS NULL");
|
|
9439
|
+
}
|
|
9440
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
9441
|
+
let limitClause = "";
|
|
9442
|
+
if (filter.limit) {
|
|
9443
|
+
limitClause = " LIMIT ?";
|
|
9444
|
+
params.push(filter.limit);
|
|
9445
|
+
if (!filter.cursor && filter.offset) {
|
|
9446
|
+
limitClause += " OFFSET ?";
|
|
9447
|
+
params.push(filter.offset);
|
|
9448
|
+
}
|
|
9449
|
+
}
|
|
9450
|
+
const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC, id ASC${limitClause}`).all(...params);
|
|
9451
|
+
return rows.map(rowToTask);
|
|
9197
9452
|
}
|
|
9198
|
-
|
|
9199
|
-
|
|
9200
|
-
|
|
9201
|
-
init_task_crud();
|
|
9202
|
-
});
|
|
9203
|
-
|
|
9204
|
-
// src/db/task-lifecycle.ts
|
|
9205
|
-
function lockExpiresAt(lockedAt) {
|
|
9206
|
-
if (!lockedAt)
|
|
9207
|
-
return null;
|
|
9208
|
-
return new Date(new Date(lockedAt).getTime() + LOCK_EXPIRY_MINUTES * 60 * 1000).toISOString();
|
|
9453
|
+
function getTaskByFingerprint(fingerprint, db) {
|
|
9454
|
+
const tasks = listTasks({ metadata: { fingerprint }, limit: 1, include_archived: true }, db);
|
|
9455
|
+
return tasks[0] ?? null;
|
|
9209
9456
|
}
|
|
9210
|
-
function
|
|
9211
|
-
|
|
9212
|
-
|
|
9213
|
-
|
|
9214
|
-
|
|
9215
|
-
|
|
9457
|
+
function mergeTaskMetadata(current, next, fingerprint) {
|
|
9458
|
+
return {
|
|
9459
|
+
...current,
|
|
9460
|
+
...next ?? {},
|
|
9461
|
+
fingerprint
|
|
9462
|
+
};
|
|
9216
9463
|
}
|
|
9217
|
-
function
|
|
9464
|
+
function upsertTaskByFingerprint(input, db) {
|
|
9218
9465
|
const d = db || getDatabase();
|
|
9219
|
-
const
|
|
9220
|
-
if (
|
|
9221
|
-
|
|
9222
|
-
const
|
|
9223
|
-
|
|
9224
|
-
const
|
|
9225
|
-
if (
|
|
9226
|
-
|
|
9227
|
-
|
|
9228
|
-
|
|
9466
|
+
const fingerprint = input.fingerprint.trim();
|
|
9467
|
+
if (!fingerprint)
|
|
9468
|
+
throw new Error("fingerprint is required");
|
|
9469
|
+
const tx = d.transaction(() => {
|
|
9470
|
+
const existing = getTaskByFingerprint(fingerprint, d);
|
|
9471
|
+
const metadata = mergeTaskMetadata(existing?.metadata ?? {}, input.metadata, fingerprint);
|
|
9472
|
+
if (!existing) {
|
|
9473
|
+
const task2 = createTask({ ...input, metadata }, d);
|
|
9474
|
+
return { task: task2, created: true };
|
|
9475
|
+
}
|
|
9476
|
+
const task = updateTask(existing.id, {
|
|
9477
|
+
version: existing.version,
|
|
9478
|
+
title: input.title,
|
|
9479
|
+
description: input.description,
|
|
9480
|
+
status: input.status,
|
|
9481
|
+
priority: input.priority,
|
|
9482
|
+
project_id: input.project_id,
|
|
9483
|
+
assigned_to: input.assigned_to,
|
|
9484
|
+
working_dir: input.working_dir,
|
|
9485
|
+
plan_id: input.plan_id,
|
|
9486
|
+
task_list_id: input.task_list_id,
|
|
9487
|
+
tags: input.tags,
|
|
9488
|
+
metadata,
|
|
9489
|
+
due_at: input.due_at,
|
|
9490
|
+
estimated_minutes: input.estimated_minutes,
|
|
9491
|
+
sla_minutes: input.sla_minutes,
|
|
9492
|
+
confidence: input.confidence,
|
|
9493
|
+
retry_count: input.retry_count,
|
|
9494
|
+
max_retries: input.max_retries,
|
|
9495
|
+
retry_after: input.retry_after,
|
|
9496
|
+
requires_approval: input.requires_approval,
|
|
9497
|
+
recurrence_rule: input.recurrence_rule,
|
|
9498
|
+
task_type: input.task_type
|
|
9499
|
+
}, d);
|
|
9500
|
+
return { task, created: false };
|
|
9501
|
+
});
|
|
9502
|
+
return tx();
|
|
9229
9503
|
}
|
|
9230
|
-
function
|
|
9504
|
+
function countTasks(filter = {}, db) {
|
|
9231
9505
|
const d = db || getDatabase();
|
|
9232
|
-
const
|
|
9233
|
-
const
|
|
9234
|
-
if (
|
|
9235
|
-
|
|
9236
|
-
|
|
9237
|
-
const blocking = getBlockingDeps(id, d);
|
|
9238
|
-
if (blocking.length > 0) {
|
|
9239
|
-
const blockerIds = blocking.map((b) => b.id.slice(0, 8)).join(", ");
|
|
9240
|
-
emitLocalEventHooksQuiet({
|
|
9241
|
-
type: "task.blocked",
|
|
9242
|
-
payload: {
|
|
9243
|
-
id,
|
|
9244
|
-
agent_id: agentId,
|
|
9245
|
-
title: task.title,
|
|
9246
|
-
blockers: blocking.map((b) => ({ id: b.id, short_id: b.short_id, title: b.title, status: b.status }))
|
|
9247
|
-
},
|
|
9248
|
-
databasePath
|
|
9249
|
-
});
|
|
9250
|
-
throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
|
|
9251
|
-
}
|
|
9252
|
-
const cutoff = lockExpiryCutoff();
|
|
9253
|
-
const timestamp = now();
|
|
9254
|
-
const result = d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, started_at = COALESCE(started_at, ?), version = version + 1, updated_at = ?
|
|
9255
|
-
WHERE id = ? AND status IN ('pending', 'in_progress') AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp, timestamp, timestamp, id, agentId, cutoff]);
|
|
9256
|
-
if (result.changes === 0) {
|
|
9257
|
-
const current = getTask(id, d);
|
|
9258
|
-
if (!current)
|
|
9259
|
-
throw new TaskNotFoundError(id);
|
|
9260
|
-
assertStartable(current, agentId);
|
|
9261
|
-
if (current.locked_by && current.locked_by !== agentId && !isLockExpired(current.locked_at)) {
|
|
9262
|
-
throw new LockError(id, current.locked_by);
|
|
9263
|
-
}
|
|
9264
|
-
throw new Error(`Task ${id} could not be started because it changed during claim`);
|
|
9506
|
+
const conditions = [];
|
|
9507
|
+
const params = [];
|
|
9508
|
+
if (filter.project_id) {
|
|
9509
|
+
conditions.push("project_id = ?");
|
|
9510
|
+
params.push(filter.project_id);
|
|
9265
9511
|
}
|
|
9266
|
-
|
|
9267
|
-
|
|
9268
|
-
|
|
9269
|
-
dispatchWebhook2("task.started", payload, d).catch(() => {});
|
|
9270
|
-
emitLocalEventHooksQuiet({ type: "task.started", payload, databasePath });
|
|
9271
|
-
emitSharedTaskEventQuiet({ type: "task.started", task: startedTask, data: { agent_id: agentId }, databasePath });
|
|
9272
|
-
return startedTask;
|
|
9273
|
-
}
|
|
9274
|
-
function completeTask(id, agentId, db, options) {
|
|
9275
|
-
const d = db || getDatabase();
|
|
9276
|
-
const databasePath = databasePathFromDatabase(d);
|
|
9277
|
-
const task = getTask(id, d);
|
|
9278
|
-
if (!task)
|
|
9279
|
-
throw new TaskNotFoundError(id);
|
|
9280
|
-
if (agentId && task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
|
|
9281
|
-
throw new LockError(id, task.locked_by);
|
|
9512
|
+
if (filter.ids && filter.ids.length > 0) {
|
|
9513
|
+
conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
|
|
9514
|
+
params.push(...filter.ids);
|
|
9282
9515
|
}
|
|
9283
|
-
|
|
9284
|
-
|
|
9285
|
-
|
|
9286
|
-
|
|
9287
|
-
|
|
9288
|
-
|
|
9289
|
-
|
|
9290
|
-
completionMeta._completion = { confidence: options.confidence };
|
|
9516
|
+
if (filter.parent_id !== undefined) {
|
|
9517
|
+
if (filter.parent_id === null) {
|
|
9518
|
+
conditions.push("parent_id IS NULL");
|
|
9519
|
+
} else {
|
|
9520
|
+
conditions.push("parent_id = ?");
|
|
9521
|
+
params.push(filter.parent_id);
|
|
9522
|
+
}
|
|
9291
9523
|
}
|
|
9292
|
-
|
|
9293
|
-
|
|
9294
|
-
|
|
9295
|
-
|
|
9296
|
-
|
|
9297
|
-
|
|
9298
|
-
|
|
9299
|
-
if (metaResult.changes === 0) {
|
|
9300
|
-
const current = getTask(id, d);
|
|
9301
|
-
throw new VersionConflictError(id, task.version, current?.version ?? -1);
|
|
9302
|
-
}
|
|
9524
|
+
if (filter.status) {
|
|
9525
|
+
if (Array.isArray(filter.status)) {
|
|
9526
|
+
conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
|
|
9527
|
+
params.push(...filter.status);
|
|
9528
|
+
} else {
|
|
9529
|
+
conditions.push("status = ?");
|
|
9530
|
+
params.push(filter.status);
|
|
9303
9531
|
}
|
|
9304
|
-
d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, confidence = ?, version = version + 1, updated_at = ?
|
|
9305
|
-
WHERE id = ?`, [timestamp, confidence, timestamp, id]);
|
|
9306
|
-
});
|
|
9307
|
-
tx();
|
|
9308
|
-
logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
|
|
9309
|
-
const completedTaskForEvent = {
|
|
9310
|
-
...task,
|
|
9311
|
-
status: "completed",
|
|
9312
|
-
locked_by: null,
|
|
9313
|
-
locked_at: null,
|
|
9314
|
-
completed_at: timestamp,
|
|
9315
|
-
confidence,
|
|
9316
|
-
version: task.version + 1,
|
|
9317
|
-
updated_at: timestamp,
|
|
9318
|
-
metadata: hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata
|
|
9319
|
-
};
|
|
9320
|
-
const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
|
|
9321
|
-
dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
|
|
9322
|
-
emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload, databasePath });
|
|
9323
|
-
emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp }, databasePath });
|
|
9324
|
-
let spawnedTask = null;
|
|
9325
|
-
if (task.recurrence_rule && !options?.skip_recurrence) {
|
|
9326
|
-
spawnedTask = spawnNextRecurrence(task, d, timestamp);
|
|
9327
9532
|
}
|
|
9328
|
-
|
|
9329
|
-
|
|
9330
|
-
|
|
9331
|
-
|
|
9332
|
-
console.warn(`[tasks] Task ${id} exceeded max spawn depth (${MAX_SPAWN_DEPTH}), skipping template spawn`);
|
|
9533
|
+
if (filter.priority) {
|
|
9534
|
+
if (Array.isArray(filter.priority)) {
|
|
9535
|
+
conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
|
|
9536
|
+
params.push(...filter.priority);
|
|
9333
9537
|
} else {
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
project_id: task.project_id ?? undefined,
|
|
9337
|
-
plan_id: task.plan_id ?? undefined,
|
|
9338
|
-
task_list_id: task.task_list_id ?? undefined,
|
|
9339
|
-
assigned_to: task.assigned_to ?? undefined
|
|
9340
|
-
}, d);
|
|
9341
|
-
input.metadata = { ...input.metadata || {}, _spawn_depth: spawnDepth + 1 };
|
|
9342
|
-
spawnedFromTemplate = createTask(input, d);
|
|
9343
|
-
} catch {}
|
|
9538
|
+
conditions.push("priority = ?");
|
|
9539
|
+
params.push(filter.priority);
|
|
9344
9540
|
}
|
|
9345
9541
|
}
|
|
9346
|
-
|
|
9347
|
-
|
|
9348
|
-
|
|
9542
|
+
if (filter.assigned_to) {
|
|
9543
|
+
conditions.push("assigned_to = ?");
|
|
9544
|
+
params.push(filter.assigned_to);
|
|
9349
9545
|
}
|
|
9350
|
-
if (
|
|
9351
|
-
|
|
9546
|
+
if (filter.agent_id) {
|
|
9547
|
+
conditions.push("agent_id = ?");
|
|
9548
|
+
params.push(filter.agent_id);
|
|
9352
9549
|
}
|
|
9353
|
-
|
|
9354
|
-
|
|
9355
|
-
|
|
9356
|
-
|
|
9357
|
-
|
|
9358
|
-
|
|
9359
|
-
|
|
9360
|
-
|
|
9361
|
-
|
|
9362
|
-
|
|
9363
|
-
|
|
9364
|
-
|
|
9365
|
-
|
|
9366
|
-
|
|
9367
|
-
|
|
9368
|
-
|
|
9369
|
-
|
|
9550
|
+
if (filter.session_id) {
|
|
9551
|
+
conditions.push("session_id = ?");
|
|
9552
|
+
params.push(filter.session_id);
|
|
9553
|
+
}
|
|
9554
|
+
if (filter.tags && filter.tags.length > 0) {
|
|
9555
|
+
const placeholders = filter.tags.map(() => "?").join(",");
|
|
9556
|
+
conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
|
|
9557
|
+
params.push(...filter.tags);
|
|
9558
|
+
}
|
|
9559
|
+
if (filter.plan_id) {
|
|
9560
|
+
conditions.push("plan_id = ?");
|
|
9561
|
+
params.push(filter.plan_id);
|
|
9562
|
+
}
|
|
9563
|
+
if (filter.task_list_id) {
|
|
9564
|
+
conditions.push("task_list_id = ?");
|
|
9565
|
+
params.push(filter.task_list_id);
|
|
9566
|
+
}
|
|
9567
|
+
if (filter.has_recurrence === true) {
|
|
9568
|
+
conditions.push("recurrence_rule IS NOT NULL");
|
|
9569
|
+
} else if (filter.has_recurrence === false) {
|
|
9570
|
+
conditions.push("recurrence_rule IS NULL");
|
|
9571
|
+
}
|
|
9572
|
+
if (filter.task_type) {
|
|
9573
|
+
if (Array.isArray(filter.task_type)) {
|
|
9574
|
+
conditions.push(`task_type IN (${filter.task_type.map(() => "?").join(",")})`);
|
|
9575
|
+
params.push(...filter.task_type);
|
|
9576
|
+
} else {
|
|
9577
|
+
conditions.push("task_type = ?");
|
|
9578
|
+
params.push(filter.task_type);
|
|
9370
9579
|
}
|
|
9371
9580
|
}
|
|
9372
|
-
|
|
9581
|
+
addMetadataConditions(filter.metadata, conditions, params);
|
|
9582
|
+
if (!filter.include_archived) {
|
|
9583
|
+
conditions.push("archived_at IS NULL");
|
|
9584
|
+
}
|
|
9585
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
9586
|
+
const row = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
|
|
9587
|
+
return row.count;
|
|
9373
9588
|
}
|
|
9374
|
-
function
|
|
9589
|
+
function updateTask(id, input, db) {
|
|
9375
9590
|
const d = db || getDatabase();
|
|
9376
9591
|
const task = getTask(id, d);
|
|
9377
9592
|
if (!task)
|
|
9378
9593
|
throw new TaskNotFoundError(id);
|
|
9379
|
-
if (task.
|
|
9380
|
-
|
|
9381
|
-
success: false,
|
|
9382
|
-
error: `Task is ${task.status} and cannot be locked`
|
|
9383
|
-
};
|
|
9384
|
-
}
|
|
9385
|
-
if (task.locked_by === agentId && !isLockExpired(task.locked_at)) {
|
|
9386
|
-
const timestamp2 = now();
|
|
9387
|
-
d.run(`UPDATE tasks SET locked_at = ?, updated_at = ?, version = version + 1 WHERE id = ? AND locked_by = ?`, [timestamp2, timestamp2, id, agentId]);
|
|
9388
|
-
logTaskChange(id, "lock_renew", "locked_by", agentId, agentId, agentId, d);
|
|
9389
|
-
return { success: true, locked_by: agentId, locked_at: timestamp2, expires_at: lockExpiresAt(timestamp2) };
|
|
9594
|
+
if (task.version !== input.version) {
|
|
9595
|
+
throw new VersionConflictError(id, input.version, task.version);
|
|
9390
9596
|
}
|
|
9391
|
-
const cutoff = lockExpiryCutoff();
|
|
9392
9597
|
const timestamp = now();
|
|
9393
|
-
const
|
|
9394
|
-
|
|
9395
|
-
|
|
9396
|
-
|
|
9397
|
-
|
|
9398
|
-
|
|
9399
|
-
|
|
9400
|
-
|
|
9401
|
-
|
|
9402
|
-
|
|
9403
|
-
|
|
9598
|
+
const completionTimestamp = input.completed_at ?? timestamp;
|
|
9599
|
+
const sets = ["version = version + 1", "updated_at = ?"];
|
|
9600
|
+
const params = [timestamp];
|
|
9601
|
+
if (input.title !== undefined) {
|
|
9602
|
+
sets.push("title = ?");
|
|
9603
|
+
params.push(input.title);
|
|
9604
|
+
}
|
|
9605
|
+
if (input.description !== undefined) {
|
|
9606
|
+
sets.push("description = ?");
|
|
9607
|
+
params.push(input.description);
|
|
9608
|
+
}
|
|
9609
|
+
if (input.status !== undefined) {
|
|
9610
|
+
if (input.status === "completed") {
|
|
9611
|
+
checkCompletionGuard(task, task.assigned_to || task.agent_id || null, d);
|
|
9404
9612
|
}
|
|
9405
|
-
|
|
9406
|
-
|
|
9407
|
-
|
|
9408
|
-
|
|
9409
|
-
|
|
9410
|
-
|
|
9411
|
-
|
|
9613
|
+
sets.push("status = ?");
|
|
9614
|
+
params.push(input.status);
|
|
9615
|
+
if (input.status === "completed") {
|
|
9616
|
+
sets.push("completed_at = ?");
|
|
9617
|
+
params.push(completionTimestamp);
|
|
9618
|
+
sets.push("locked_by = NULL");
|
|
9619
|
+
sets.push("locked_at = NULL");
|
|
9620
|
+
} else if (task.status === "completed" && input.completed_at === undefined) {
|
|
9621
|
+
sets.push("completed_at = NULL");
|
|
9412
9622
|
}
|
|
9413
|
-
return {
|
|
9414
|
-
success: false,
|
|
9415
|
-
error: `Task ${id} could not be locked because it changed during lock acquisition`
|
|
9416
|
-
};
|
|
9417
9623
|
}
|
|
9418
|
-
|
|
9419
|
-
|
|
9420
|
-
|
|
9421
|
-
|
|
9422
|
-
|
|
9423
|
-
|
|
9424
|
-
|
|
9425
|
-
|
|
9426
|
-
if (
|
|
9427
|
-
|
|
9624
|
+
if (input.priority !== undefined) {
|
|
9625
|
+
sets.push("priority = ?");
|
|
9626
|
+
params.push(input.priority);
|
|
9627
|
+
}
|
|
9628
|
+
if (input.project_id !== undefined) {
|
|
9629
|
+
sets.push("project_id = ?");
|
|
9630
|
+
params.push(input.project_id);
|
|
9631
|
+
}
|
|
9632
|
+
if (input.assigned_to !== undefined) {
|
|
9633
|
+
sets.push("assigned_to = ?");
|
|
9634
|
+
params.push(input.assigned_to);
|
|
9635
|
+
}
|
|
9636
|
+
if (input.working_dir !== undefined) {
|
|
9637
|
+
sets.push("working_dir = ?");
|
|
9638
|
+
params.push(input.working_dir);
|
|
9639
|
+
}
|
|
9640
|
+
if (input.tags !== undefined) {
|
|
9641
|
+
sets.push("tags = ?");
|
|
9642
|
+
params.push(JSON.stringify(input.tags));
|
|
9643
|
+
}
|
|
9644
|
+
if (input.metadata !== undefined) {
|
|
9645
|
+
sets.push("metadata = ?");
|
|
9646
|
+
params.push(JSON.stringify(input.metadata));
|
|
9647
|
+
}
|
|
9648
|
+
if (input.plan_id !== undefined) {
|
|
9649
|
+
sets.push("plan_id = ?");
|
|
9650
|
+
params.push(input.plan_id);
|
|
9651
|
+
}
|
|
9652
|
+
if (input.task_list_id !== undefined) {
|
|
9653
|
+
sets.push("task_list_id = ?");
|
|
9654
|
+
params.push(input.task_list_id);
|
|
9655
|
+
}
|
|
9656
|
+
if (input.due_at !== undefined) {
|
|
9657
|
+
sets.push("due_at = ?");
|
|
9658
|
+
params.push(input.due_at);
|
|
9659
|
+
}
|
|
9660
|
+
if (input.estimated_minutes !== undefined) {
|
|
9661
|
+
sets.push("estimated_minutes = ?");
|
|
9662
|
+
params.push(input.estimated_minutes);
|
|
9663
|
+
}
|
|
9664
|
+
if (input.sla_minutes !== undefined) {
|
|
9665
|
+
sets.push("sla_minutes = ?");
|
|
9666
|
+
params.push(input.sla_minutes);
|
|
9428
9667
|
}
|
|
9429
|
-
|
|
9430
|
-
|
|
9431
|
-
|
|
9432
|
-
return true;
|
|
9433
|
-
}
|
|
9434
|
-
function getTaskLockStatus(id, db) {
|
|
9435
|
-
const d = db || getDatabase();
|
|
9436
|
-
const task = getTask(id, d);
|
|
9437
|
-
if (!task)
|
|
9438
|
-
throw new TaskNotFoundError(id);
|
|
9439
|
-
const expired = isLockExpired(task.locked_at);
|
|
9440
|
-
return {
|
|
9441
|
-
task_id: id,
|
|
9442
|
-
locked: !!task.locked_by && !expired,
|
|
9443
|
-
locked_by: task.locked_by,
|
|
9444
|
-
locked_at: task.locked_at,
|
|
9445
|
-
expires_at: lockExpiresAt(task.locked_at),
|
|
9446
|
-
expired
|
|
9447
|
-
};
|
|
9448
|
-
}
|
|
9449
|
-
function claimNextTask(agentId, filters, db) {
|
|
9450
|
-
const d = db || getDatabase();
|
|
9451
|
-
const tx = d.transaction(() => {
|
|
9452
|
-
const task = getNextTask(agentId, filters, d);
|
|
9453
|
-
if (!task)
|
|
9454
|
-
return null;
|
|
9455
|
-
return startTask(task.id, agentId, d);
|
|
9456
|
-
});
|
|
9457
|
-
return tx();
|
|
9458
|
-
}
|
|
9459
|
-
function getNextTask(agentId, filters, db) {
|
|
9460
|
-
const d = db || getDatabase();
|
|
9461
|
-
clearExpiredLocks(d);
|
|
9462
|
-
const conditions = ["status = 'pending'", "(locked_by IS NULL OR locked_at < ?)"];
|
|
9463
|
-
const params = [lockExpiryCutoff()];
|
|
9464
|
-
if (filters?.project_id) {
|
|
9465
|
-
conditions.push("project_id = ?");
|
|
9466
|
-
params.push(filters.project_id);
|
|
9668
|
+
if (input.actual_minutes !== undefined) {
|
|
9669
|
+
sets.push("actual_minutes = ?");
|
|
9670
|
+
params.push(input.actual_minutes);
|
|
9467
9671
|
}
|
|
9468
|
-
if (
|
|
9469
|
-
|
|
9470
|
-
params.push(
|
|
9672
|
+
if (input.completed_at !== undefined && input.status !== "completed") {
|
|
9673
|
+
sets.push("completed_at = ?");
|
|
9674
|
+
params.push(input.completed_at);
|
|
9471
9675
|
}
|
|
9472
|
-
if (
|
|
9473
|
-
|
|
9474
|
-
params.push(
|
|
9676
|
+
if (input.confidence !== undefined) {
|
|
9677
|
+
sets.push("confidence = ?");
|
|
9678
|
+
params.push(input.confidence);
|
|
9475
9679
|
}
|
|
9476
|
-
if (
|
|
9477
|
-
|
|
9478
|
-
|
|
9479
|
-
params.push(...filters.tags);
|
|
9680
|
+
if (input.retry_count !== undefined) {
|
|
9681
|
+
sets.push("retry_count = ?");
|
|
9682
|
+
params.push(input.retry_count);
|
|
9480
9683
|
}
|
|
9481
|
-
|
|
9482
|
-
|
|
9483
|
-
|
|
9484
|
-
if (agentId) {
|
|
9485
|
-
const recentRows = d.query(`SELECT DISTINCT project_id FROM tasks WHERE assigned_to = ? AND status = 'completed' AND project_id IS NOT NULL ORDER BY completed_at DESC LIMIT 3`).all(agentId);
|
|
9486
|
-
recentProjectIds = recentRows.map((r) => r.project_id);
|
|
9684
|
+
if (input.max_retries !== undefined) {
|
|
9685
|
+
sets.push("max_retries = ?");
|
|
9686
|
+
params.push(input.max_retries);
|
|
9487
9687
|
}
|
|
9488
|
-
|
|
9489
|
-
|
|
9490
|
-
|
|
9491
|
-
params.push(agentId);
|
|
9688
|
+
if (input.retry_after !== undefined) {
|
|
9689
|
+
sets.push("retry_after = ?");
|
|
9690
|
+
params.push(input.retry_after);
|
|
9492
9691
|
}
|
|
9493
|
-
if (
|
|
9494
|
-
|
|
9495
|
-
|
|
9496
|
-
params.push(...recentProjectIds);
|
|
9692
|
+
if (input.requires_approval !== undefined) {
|
|
9693
|
+
sets.push("requires_approval = ?");
|
|
9694
|
+
params.push(input.requires_approval ? 1 : 0);
|
|
9497
9695
|
}
|
|
9498
|
-
|
|
9499
|
-
|
|
9500
|
-
|
|
9501
|
-
|
|
9502
|
-
|
|
9503
|
-
const d = db || getDatabase();
|
|
9504
|
-
clearExpiredLocks(d);
|
|
9505
|
-
const conditions = ["status = 'in_progress'"];
|
|
9506
|
-
const params = [];
|
|
9507
|
-
if (filters?.project_id) {
|
|
9508
|
-
conditions.push("project_id = ?");
|
|
9509
|
-
params.push(filters.project_id);
|
|
9696
|
+
if (input.approved_by !== undefined) {
|
|
9697
|
+
sets.push("approved_by = ?");
|
|
9698
|
+
params.push(input.approved_by);
|
|
9699
|
+
sets.push("approved_at = ?");
|
|
9700
|
+
params.push(now());
|
|
9510
9701
|
}
|
|
9511
|
-
if (
|
|
9512
|
-
|
|
9513
|
-
params.push(
|
|
9702
|
+
if (input.recurrence_rule !== undefined) {
|
|
9703
|
+
sets.push("recurrence_rule = ?");
|
|
9704
|
+
params.push(input.recurrence_rule);
|
|
9514
9705
|
}
|
|
9515
|
-
|
|
9516
|
-
|
|
9517
|
-
|
|
9518
|
-
updated_at DESC`).all(...params);
|
|
9519
|
-
return rows;
|
|
9520
|
-
}
|
|
9521
|
-
function getTasksChangedSince(since, filters, db) {
|
|
9522
|
-
const d = db || getDatabase();
|
|
9523
|
-
const conditions = ["updated_at > ?"];
|
|
9524
|
-
const params = [since];
|
|
9525
|
-
if (filters?.project_id) {
|
|
9526
|
-
conditions.push("project_id = ?");
|
|
9527
|
-
params.push(filters.project_id);
|
|
9706
|
+
if (input.task_type !== undefined) {
|
|
9707
|
+
sets.push("task_type = ?");
|
|
9708
|
+
params.push(input.task_type ?? null);
|
|
9528
9709
|
}
|
|
9529
|
-
|
|
9530
|
-
|
|
9531
|
-
|
|
9710
|
+
params.push(id, input.version);
|
|
9711
|
+
const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
|
|
9712
|
+
if (result.changes === 0) {
|
|
9713
|
+
const current = getTask(id, d);
|
|
9714
|
+
throw new VersionConflictError(id, input.version, current?.version ?? -1);
|
|
9532
9715
|
}
|
|
9533
|
-
|
|
9534
|
-
|
|
9535
|
-
|
|
9536
|
-
|
|
9537
|
-
|
|
9538
|
-
|
|
9539
|
-
|
|
9540
|
-
|
|
9541
|
-
|
|
9542
|
-
|
|
9543
|
-
const meta = {
|
|
9544
|
-
...task.metadata,
|
|
9545
|
-
_failure: {
|
|
9546
|
-
reason: reason || "Unknown failure",
|
|
9547
|
-
error_code: options?.error_code || null,
|
|
9548
|
-
failed_by: agentId || null,
|
|
9549
|
-
failed_at: now(),
|
|
9550
|
-
retry_requested: options?.retry || false
|
|
9716
|
+
if (input.tags !== undefined) {
|
|
9717
|
+
replaceTaskTags(id, input.tags, d);
|
|
9718
|
+
}
|
|
9719
|
+
const transitionedToCompleted = input.status === "completed" && task.status !== "completed";
|
|
9720
|
+
if (transitionedToCompleted && task.recurrence_rule) {
|
|
9721
|
+
try {
|
|
9722
|
+
const { spawnNextRecurrence: spawnNextRecurrence2 } = (init_task_lifecycle(), __toCommonJS(exports_task_lifecycle));
|
|
9723
|
+
spawnNextRecurrence2(task, d, completionTimestamp);
|
|
9724
|
+
} catch (e) {
|
|
9725
|
+
console.warn(`[tasks] failed to spawn next recurrence for ${id}: ${e instanceof Error ? e.message : String(e)}`);
|
|
9551
9726
|
}
|
|
9552
|
-
}
|
|
9553
|
-
const
|
|
9554
|
-
|
|
9555
|
-
|
|
9556
|
-
|
|
9727
|
+
}
|
|
9728
|
+
const agentId = task.assigned_to || task.agent_id || null;
|
|
9729
|
+
if (input.status !== undefined && input.status !== task.status)
|
|
9730
|
+
logTaskChange(id, "update", "status", task.status, input.status, agentId, d);
|
|
9731
|
+
if (input.priority !== undefined && input.priority !== task.priority)
|
|
9732
|
+
logTaskChange(id, "update", "priority", task.priority, input.priority, agentId, d);
|
|
9733
|
+
if (input.title !== undefined && input.title !== task.title)
|
|
9734
|
+
logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
|
|
9735
|
+
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
|
|
9736
|
+
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
9737
|
+
if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
|
|
9738
|
+
logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
|
|
9739
|
+
if (input.approved_by !== undefined)
|
|
9740
|
+
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
9741
|
+
const reopened = input.status !== undefined && input.status !== "completed" && task.status === "completed" && input.completed_at === undefined;
|
|
9742
|
+
const completedNow = input.status === "completed";
|
|
9743
|
+
const updatedTask = {
|
|
9557
9744
|
...task,
|
|
9558
|
-
|
|
9559
|
-
|
|
9560
|
-
|
|
9561
|
-
metadata: meta,
|
|
9745
|
+
...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
|
|
9746
|
+
tags: input.tags ?? task.tags,
|
|
9747
|
+
metadata: input.metadata ?? task.metadata,
|
|
9562
9748
|
version: task.version + 1,
|
|
9563
|
-
updated_at: timestamp
|
|
9749
|
+
updated_at: timestamp,
|
|
9750
|
+
locked_by: completedNow ? null : task.locked_by,
|
|
9751
|
+
locked_at: completedNow ? null : task.locked_at,
|
|
9752
|
+
completed_at: completedNow ? completionTimestamp : reopened ? null : input.completed_at !== undefined ? input.completed_at : task.completed_at,
|
|
9753
|
+
sla_minutes: input.sla_minutes !== undefined ? input.sla_minutes : task.sla_minutes,
|
|
9754
|
+
actual_minutes: input.actual_minutes ?? task.actual_minutes,
|
|
9755
|
+
confidence: input.confidence !== undefined ? input.confidence : task.confidence,
|
|
9756
|
+
retry_count: input.retry_count ?? task.retry_count,
|
|
9757
|
+
max_retries: input.max_retries ?? task.max_retries,
|
|
9758
|
+
retry_after: input.retry_after !== undefined ? input.retry_after : task.retry_after,
|
|
9759
|
+
requires_approval: input.requires_approval !== undefined ? input.requires_approval : task.requires_approval,
|
|
9760
|
+
approved_by: input.approved_by ?? task.approved_by,
|
|
9761
|
+
approved_at: input.approved_by ? timestamp : task.approved_at
|
|
9564
9762
|
};
|
|
9565
|
-
|
|
9566
|
-
|
|
9567
|
-
|
|
9568
|
-
|
|
9569
|
-
|
|
9570
|
-
|
|
9571
|
-
if (options?.retry) {
|
|
9572
|
-
const retryCount = (task.retry_count || 0) + 1;
|
|
9573
|
-
const maxRetries = task.max_retries || 3;
|
|
9574
|
-
if (retryCount > maxRetries) {
|
|
9575
|
-
d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [
|
|
9576
|
-
JSON.stringify({ ...meta, _retry_exhausted: { retry_count: retryCount - 1, max_retries: maxRetries } }),
|
|
9577
|
-
id
|
|
9578
|
-
]);
|
|
9579
|
-
} else {
|
|
9580
|
-
const backoffMinutes = Math.pow(5, retryCount - 1);
|
|
9581
|
-
const retryAfter = options.retry_after || new Date(Date.now() + backoffMinutes * 60 * 1000).toISOString();
|
|
9582
|
-
let title = task.title;
|
|
9583
|
-
if (task.short_id && title.startsWith(task.short_id + ": ")) {
|
|
9584
|
-
title = title.slice(task.short_id.length + 2);
|
|
9585
|
-
}
|
|
9586
|
-
retryTask = createTask({
|
|
9587
|
-
title,
|
|
9588
|
-
description: task.description ?? undefined,
|
|
9589
|
-
priority: task.priority,
|
|
9590
|
-
project_id: task.project_id ?? undefined,
|
|
9591
|
-
task_list_id: task.task_list_id ?? undefined,
|
|
9592
|
-
plan_id: task.plan_id ?? undefined,
|
|
9593
|
-
assigned_to: task.assigned_to ?? undefined,
|
|
9594
|
-
tags: task.tags,
|
|
9595
|
-
metadata: { ...task.metadata, _retry: { original_id: task.id, retry_count: retryCount, max_retries: maxRetries, retry_after: retryAfter, failure_reason: reason } },
|
|
9596
|
-
estimated_minutes: task.estimated_minutes ?? undefined,
|
|
9597
|
-
recurrence_rule: task.recurrence_rule ?? undefined,
|
|
9598
|
-
due_at: retryAfter
|
|
9599
|
-
}, d);
|
|
9600
|
-
d.run("UPDATE tasks SET retry_count = ?, max_retries = ?, retry_after = ? WHERE id = ?", [retryCount, maxRetries, retryAfter, retryTask.id]);
|
|
9601
|
-
}
|
|
9763
|
+
const databasePath = databasePathFromDatabase(d);
|
|
9764
|
+
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
|
|
9765
|
+
const payload = taskEventData(updatedTask, { assigned_to: input.assigned_to, old_assigned_to: task.assigned_to });
|
|
9766
|
+
dispatchWebhook2("task.assigned", payload, d).catch(() => {});
|
|
9767
|
+
emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
|
|
9768
|
+
emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to }, databasePath });
|
|
9602
9769
|
}
|
|
9603
|
-
|
|
9604
|
-
}
|
|
9605
|
-
|
|
9606
|
-
|
|
9607
|
-
|
|
9608
|
-
const effectiveFilters = typeof staleQuery === "number" ? filters : { project_id: staleQuery.project_id, task_list_id: staleQuery.task_list_id };
|
|
9609
|
-
const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
|
|
9610
|
-
const conditions = [
|
|
9611
|
-
"status = 'in_progress'",
|
|
9612
|
-
"(updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))"
|
|
9613
|
-
];
|
|
9614
|
-
const params = [cutoff, cutoff];
|
|
9615
|
-
if (effectiveFilters?.project_id) {
|
|
9616
|
-
conditions.push("project_id = ?");
|
|
9617
|
-
params.push(effectiveFilters.project_id);
|
|
9770
|
+
if (input.status !== undefined && input.status !== task.status) {
|
|
9771
|
+
const payload = taskEventData(updatedTask, { old_status: task.status, new_status: input.status });
|
|
9772
|
+
dispatchWebhook2("task.status_changed", payload, d).catch(() => {});
|
|
9773
|
+
emitLocalEventHooksQuiet({ type: "task.status_changed", payload, databasePath });
|
|
9774
|
+
emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status }, databasePath });
|
|
9618
9775
|
}
|
|
9619
|
-
if (
|
|
9620
|
-
|
|
9621
|
-
params.push(effectiveFilters.task_list_id);
|
|
9776
|
+
if (input.approved_by !== undefined) {
|
|
9777
|
+
emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title }, databasePath });
|
|
9622
9778
|
}
|
|
9623
|
-
const
|
|
9624
|
-
|
|
9625
|
-
|
|
9626
|
-
}
|
|
9627
|
-
|
|
9628
|
-
const d = db || getDatabase();
|
|
9629
|
-
const databasePath = databasePathFromDatabase(d);
|
|
9630
|
-
const staleMinutes = opts?.stale_minutes ?? 30;
|
|
9631
|
-
const staleTasks = getStaleTasks(staleMinutes, { project_id: opts?.project_id, task_list_id: opts?.task_list_id }, d);
|
|
9632
|
-
if (staleTasks.length === 0)
|
|
9633
|
-
return null;
|
|
9634
|
-
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
9635
|
-
staleTasks.sort((a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9));
|
|
9636
|
-
const target = staleTasks[0];
|
|
9637
|
-
const timestamp = now();
|
|
9638
|
-
const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
|
|
9639
|
-
const result = d.run(`UPDATE tasks SET assigned_to = ?, locked_by = ?, locked_at = ?, updated_at = ?, version = version + 1
|
|
9640
|
-
WHERE id = ? AND status = 'in_progress' AND (updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))`, [agentId, agentId, timestamp, timestamp, target.id, cutoff, cutoff]);
|
|
9641
|
-
if (result.changes === 0)
|
|
9642
|
-
return null;
|
|
9643
|
-
logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
|
|
9644
|
-
logTaskChange(target.id, "steal", "locked_by", target.locked_by, agentId, agentId, d);
|
|
9645
|
-
const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
|
|
9646
|
-
const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
|
|
9647
|
-
dispatchWebhook2("task.assigned", payload, d).catch(() => {});
|
|
9648
|
-
emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
|
|
9649
|
-
emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to }, databasePath });
|
|
9650
|
-
return stolenTask;
|
|
9779
|
+
const updatePayload = taskEventData(updatedTask);
|
|
9780
|
+
dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
|
|
9781
|
+
emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload, databasePath });
|
|
9782
|
+
emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask, databasePath });
|
|
9783
|
+
return updatedTask;
|
|
9651
9784
|
}
|
|
9652
|
-
function
|
|
9785
|
+
function deleteTask(id, db) {
|
|
9653
9786
|
const d = db || getDatabase();
|
|
9654
|
-
const
|
|
9655
|
-
|
|
9656
|
-
|
|
9657
|
-
|
|
9658
|
-
|
|
9659
|
-
|
|
9660
|
-
|
|
9661
|
-
|
|
9662
|
-
|
|
9663
|
-
|
|
9664
|
-
|
|
9665
|
-
return tx();
|
|
9666
|
-
}
|
|
9667
|
-
function spawnNextRecurrence(completedTask, db, completedAt) {
|
|
9668
|
-
const recurrenceBase = completedTask.due_at ? new Date(completedTask.due_at) : new Date(completedAt);
|
|
9669
|
-
const dueAt = nextOccurrence(completedTask.recurrence_rule, recurrenceBase);
|
|
9670
|
-
let title = completedTask.title;
|
|
9671
|
-
if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
|
|
9672
|
-
title = title.slice(completedTask.short_id.length + 2);
|
|
9673
|
-
}
|
|
9674
|
-
const recurrenceParentId = completedTask.recurrence_parent_id || completedTask.id;
|
|
9675
|
-
return createTask({
|
|
9676
|
-
title,
|
|
9677
|
-
description: completedTask.description ?? undefined,
|
|
9678
|
-
priority: completedTask.priority,
|
|
9679
|
-
project_id: completedTask.project_id ?? undefined,
|
|
9680
|
-
task_list_id: completedTask.task_list_id ?? undefined,
|
|
9681
|
-
plan_id: completedTask.plan_id ?? undefined,
|
|
9682
|
-
assigned_to: completedTask.assigned_to ?? undefined,
|
|
9683
|
-
tags: completedTask.tags,
|
|
9684
|
-
metadata: completedTask.metadata,
|
|
9685
|
-
estimated_minutes: completedTask.estimated_minutes ?? undefined,
|
|
9686
|
-
sla_minutes: completedTask.sla_minutes ?? undefined,
|
|
9687
|
-
recurrence_rule: completedTask.recurrence_rule,
|
|
9688
|
-
recurrence_parent_id: recurrenceParentId,
|
|
9689
|
-
due_at: dueAt
|
|
9690
|
-
}, db);
|
|
9787
|
+
const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
|
|
9788
|
+
if (!row)
|
|
9789
|
+
return false;
|
|
9790
|
+
recordStorageTombstone({
|
|
9791
|
+
object_type: "tasks",
|
|
9792
|
+
object_id: id,
|
|
9793
|
+
payload: rowToTask(row),
|
|
9794
|
+
version: row.version
|
|
9795
|
+
}, d);
|
|
9796
|
+
const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
|
|
9797
|
+
return result.changes > 0;
|
|
9691
9798
|
}
|
|
9692
|
-
var
|
|
9693
|
-
var init_task_lifecycle = __esm(() => {
|
|
9799
|
+
var init_task_crud = __esm(() => {
|
|
9694
9800
|
init_types();
|
|
9695
9801
|
init_database();
|
|
9696
9802
|
init_completion_guard();
|
|
@@ -9698,11 +9804,9 @@ var init_task_lifecycle = __esm(() => {
|
|
|
9698
9804
|
init_event_hooks();
|
|
9699
9805
|
init_shared_events();
|
|
9700
9806
|
init_audit();
|
|
9701
|
-
init_recurrence();
|
|
9702
9807
|
init_webhooks();
|
|
9703
|
-
|
|
9704
|
-
|
|
9705
|
-
init_task_graph();
|
|
9808
|
+
init_checklists();
|
|
9809
|
+
init_storage_tombstones();
|
|
9706
9810
|
});
|
|
9707
9811
|
|
|
9708
9812
|
// src/db/task-status.ts
|
|
@@ -9792,6 +9896,15 @@ function setTaskStatus(id, status, _agentId, db) {
|
|
|
9792
9896
|
throw new TaskNotFoundError(id);
|
|
9793
9897
|
if (task.status === status)
|
|
9794
9898
|
return task;
|
|
9899
|
+
if (status === "completed") {
|
|
9900
|
+
try {
|
|
9901
|
+
return completeTask(id, _agentId, d);
|
|
9902
|
+
} catch (e) {
|
|
9903
|
+
if (e instanceof VersionConflictError && attempt < 2)
|
|
9904
|
+
continue;
|
|
9905
|
+
throw e;
|
|
9906
|
+
}
|
|
9907
|
+
}
|
|
9795
9908
|
try {
|
|
9796
9909
|
return updateTask(id, { status, version: task.version }, d);
|
|
9797
9910
|
} catch (e) {
|
|
@@ -12940,20 +13053,56 @@ import chalk2 from "chalk";
|
|
|
12940
13053
|
import { basename as basename3, resolve as resolve9 } from "path";
|
|
12941
13054
|
function resolveProjectIdOrSlug(input) {
|
|
12942
13055
|
const db = getDatabase();
|
|
13056
|
+
if (isPathLike(input)) {
|
|
13057
|
+
const projectPath = resolve9(input);
|
|
13058
|
+
const byPath2 = getProjectByPath(projectPath, db);
|
|
13059
|
+
return (byPath2 ?? ensureProject(basename3(projectPath), projectPath, db)).id;
|
|
13060
|
+
}
|
|
13061
|
+
const byPath = getProjectByPath(resolve9(input), db);
|
|
13062
|
+
if (byPath)
|
|
13063
|
+
return byPath.id;
|
|
12943
13064
|
const byId = getProject(input, db);
|
|
12944
13065
|
if (byId)
|
|
12945
13066
|
return byId.id;
|
|
12946
|
-
const
|
|
13067
|
+
const partial = resolvePartialId(db, "projects", input);
|
|
13068
|
+
if (partial)
|
|
13069
|
+
return partial;
|
|
13070
|
+
const exact = db.query("SELECT id FROM projects WHERE lower(name) = lower(?) OR task_list_id = ? ORDER BY name LIMIT 1").get(input, input);
|
|
13071
|
+
if (exact)
|
|
13072
|
+
return exact.id;
|
|
13073
|
+
const inputSlug = slugify(input);
|
|
13074
|
+
if (inputSlug) {
|
|
13075
|
+
const all = db.query("SELECT id, name FROM projects ORDER BY name").all();
|
|
13076
|
+
const bySlug = all.find((p) => slugify(p.name) === inputSlug);
|
|
13077
|
+
if (bySlug)
|
|
13078
|
+
return bySlug.id;
|
|
13079
|
+
}
|
|
13080
|
+
const row = db.query("SELECT id FROM projects WHERE name LIKE ? ORDER BY name LIMIT 1").get(`%${input}%`);
|
|
12947
13081
|
if (row)
|
|
12948
13082
|
return row.id;
|
|
12949
|
-
if (isPathLike(input)) {
|
|
12950
|
-
const projectPath = resolve9(input);
|
|
12951
|
-
const byPath = getProjectByPath(projectPath, db);
|
|
12952
|
-
return (byPath ?? ensureProject(basename3(projectPath), projectPath, db)).id;
|
|
12953
|
-
}
|
|
12954
13083
|
console.error(chalk2.red(`Project not found: ${input}`));
|
|
12955
13084
|
process.exit(1);
|
|
12956
13085
|
}
|
|
13086
|
+
function parseStatus(value) {
|
|
13087
|
+
if (!value)
|
|
13088
|
+
return;
|
|
13089
|
+
const normalized = normalizeStatus(value);
|
|
13090
|
+
if (!TASK_STATUSES.includes(normalized)) {
|
|
13091
|
+
console.error(chalk2.red(`--status must be one of: ${TASK_STATUSES.join(", ")}`));
|
|
13092
|
+
process.exit(1);
|
|
13093
|
+
}
|
|
13094
|
+
return normalized;
|
|
13095
|
+
}
|
|
13096
|
+
function parseIntOption(value, flag) {
|
|
13097
|
+
if (value === undefined)
|
|
13098
|
+
return;
|
|
13099
|
+
const n = parseInt(value, 10);
|
|
13100
|
+
if (!Number.isFinite(n)) {
|
|
13101
|
+
console.error(chalk2.red(`${flag} must be a number`));
|
|
13102
|
+
process.exit(1);
|
|
13103
|
+
}
|
|
13104
|
+
return n;
|
|
13105
|
+
}
|
|
12957
13106
|
function isPathLike(input) {
|
|
12958
13107
|
return input.startsWith(".") || input.includes("/") || input.includes("\\");
|
|
12959
13108
|
}
|
|
@@ -13052,27 +13201,32 @@ function registerTaskCommands(program2) {
|
|
|
13052
13201
|
}
|
|
13053
13202
|
return id;
|
|
13054
13203
|
})() : undefined;
|
|
13055
|
-
|
|
13056
|
-
|
|
13057
|
-
|
|
13058
|
-
|
|
13059
|
-
|
|
13060
|
-
|
|
13061
|
-
|
|
13062
|
-
|
|
13063
|
-
|
|
13064
|
-
|
|
13065
|
-
|
|
13066
|
-
|
|
13067
|
-
|
|
13068
|
-
|
|
13069
|
-
|
|
13070
|
-
|
|
13071
|
-
|
|
13072
|
-
|
|
13073
|
-
|
|
13074
|
-
|
|
13075
|
-
|
|
13204
|
+
let task2;
|
|
13205
|
+
try {
|
|
13206
|
+
task2 = createTask({
|
|
13207
|
+
title,
|
|
13208
|
+
description: opts.description,
|
|
13209
|
+
priority: parsePriority(opts.priority),
|
|
13210
|
+
parent_id: opts.parent ? resolveTaskId(opts.parent) : undefined,
|
|
13211
|
+
tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : undefined,
|
|
13212
|
+
plan_id: opts.plan ? resolvePlanId(opts.plan) : undefined,
|
|
13213
|
+
assigned_to: opts.assign,
|
|
13214
|
+
status: parseStatus(opts.status),
|
|
13215
|
+
task_list_id: taskListId,
|
|
13216
|
+
agent_id: globalOpts.agent,
|
|
13217
|
+
session_id: globalOpts.session,
|
|
13218
|
+
project_id: projectId,
|
|
13219
|
+
working_dir: process.cwd(),
|
|
13220
|
+
estimated_minutes: parseIntOption(opts.estimated, "--estimated"),
|
|
13221
|
+
sla_minutes: opts.slaMinutes !== undefined || opts.sla !== undefined ? parseIntOption(opts.slaMinutes ?? opts.sla, "--sla-minutes") : undefined,
|
|
13222
|
+
requires_approval: opts.approval || false,
|
|
13223
|
+
recurrence_rule: opts.recurrence,
|
|
13224
|
+
due_at: opts.due ? opts.due.length === 10 ? opts.due + "T00:00:00.000Z" : opts.due : undefined,
|
|
13225
|
+
reason: opts.reason
|
|
13226
|
+
});
|
|
13227
|
+
} catch (e) {
|
|
13228
|
+
handleError(e);
|
|
13229
|
+
}
|
|
13076
13230
|
if (globalOpts.json) {
|
|
13077
13231
|
output(task2, true);
|
|
13078
13232
|
} else {
|
|
@@ -13103,7 +13257,7 @@ function registerTaskCommands(program2) {
|
|
|
13103
13257
|
title: opts.title,
|
|
13104
13258
|
description: opts.description,
|
|
13105
13259
|
priority: parsePriority(opts.priority),
|
|
13106
|
-
status:
|
|
13260
|
+
status: parseStatus(opts.status),
|
|
13107
13261
|
task_list_id: taskListId,
|
|
13108
13262
|
tags: parseTags(opts.tags),
|
|
13109
13263
|
metadata: buildExpectationMetadata(opts),
|
|
@@ -13580,7 +13734,7 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
|
|
|
13580
13734
|
console.log(` ${chalk2.dim(h.created_at)} ${chalk2.bold(h.action)}${field}${change}${agent}`);
|
|
13581
13735
|
}
|
|
13582
13736
|
});
|
|
13583
|
-
program2.command("update <id>").description("Update a task").option("--title <text>", "New title").option("-d, --description <text>", "New description").option("-s, --status <status>", "New status").option("-p, --priority <priority>", "New priority").option("--assign <agent>", "Assign to agent").option("--tags <tags>", "New tags (comma-separated)").option("--tag <tags>", "New tags (alias for --tags)").option("--list <id>", "Move to a task list").option("--task-list <id>", "Move to a task list (alias for --list)").option("--plan <id>", "Move to a plan").option("--clear-plan", "Remove from its current plan").option("--estimated <minutes>", "Estimated time in minutes").option("--sla-minutes <minutes>", "SLA minutes before unfinished work is escalated").option("--sla <minutes>", "Alias for --sla-minutes").option("--due <date>", "Due date (ISO string or YYYY-MM-DD), empty to clear").option("--recurrence <rule>", "Recurrence rule, empty to clear").option("--approval", "Require approval before completion").action((id, opts) => {
|
|
13737
|
+
program2.command("update <id>").description("Update a task").option("--title <text>", "New title").option("-d, --description <text>", "New description").option("-s, --status <status>", "New status").option("-p, --priority <priority>", "New priority").option("--assign <agent>", "Assign to agent").option("--tags <tags>", "New tags (comma-separated)").option("--tag <tags>", "New tags (alias for --tags)").option("--list <id>", "Move to a task list").option("--task-list <id>", "Move to a task list (alias for --list)").option("--plan <id>", "Move to a plan").option("--clear-plan", "Remove from its current plan").option("--estimated <minutes>", "Estimated time in minutes").option("--sla-minutes <minutes>", "SLA minutes before unfinished work is escalated").option("--sla <minutes>", "Alias for --sla-minutes").option("--due <date>", "Due date (ISO string or YYYY-MM-DD), empty to clear").option("--recurrence <rule>", "Recurrence rule, empty to clear").option("--approval", "Require approval before completion").option("--clear-approval", "Remove the approval requirement").action((id, opts) => {
|
|
13584
13738
|
const globalOpts = program2.opts();
|
|
13585
13739
|
opts.tags = opts.tags || opts.tag;
|
|
13586
13740
|
opts.list = opts.list || opts.taskList;
|
|
@@ -13594,6 +13748,10 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
|
|
|
13594
13748
|
console.error(chalk2.red("Use either --plan or --clear-plan, not both."));
|
|
13595
13749
|
process.exit(1);
|
|
13596
13750
|
}
|
|
13751
|
+
if (opts.approval && opts.clearApproval) {
|
|
13752
|
+
console.error(chalk2.red("Use either --approval or --clear-approval, not both."));
|
|
13753
|
+
process.exit(1);
|
|
13754
|
+
}
|
|
13597
13755
|
const taskListId = opts.list ? (() => {
|
|
13598
13756
|
const db = getDatabase();
|
|
13599
13757
|
const resolved = resolvePartialId(db, "task_lists", opts.list);
|
|
@@ -13610,17 +13768,17 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
|
|
|
13610
13768
|
version: current.version,
|
|
13611
13769
|
title: opts.title,
|
|
13612
13770
|
description: opts.description,
|
|
13613
|
-
status:
|
|
13614
|
-
priority: opts.priority,
|
|
13771
|
+
status: parseStatus(opts.status),
|
|
13772
|
+
priority: parsePriority(opts.priority),
|
|
13615
13773
|
assigned_to: opts.assign,
|
|
13616
13774
|
tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : undefined,
|
|
13617
13775
|
plan_id: planId,
|
|
13618
13776
|
task_list_id: taskListId,
|
|
13619
|
-
estimated_minutes: opts.estimated !== undefined ?
|
|
13620
|
-
sla_minutes: opts.slaMinutes !== undefined || opts.sla !== undefined ?
|
|
13777
|
+
estimated_minutes: opts.estimated !== undefined ? parseIntOption(opts.estimated, "--estimated") : undefined,
|
|
13778
|
+
sla_minutes: opts.slaMinutes !== undefined || opts.sla !== undefined ? parseIntOption(opts.slaMinutes ?? opts.sla, "--sla-minutes") : undefined,
|
|
13621
13779
|
due_at: opts.due !== undefined ? opts.due === "" ? null : opts.due.length === 10 ? opts.due + "T00:00:00.000Z" : opts.due : undefined,
|
|
13622
13780
|
recurrence_rule: opts.recurrence !== undefined ? opts.recurrence === "" ? null : opts.recurrence : undefined,
|
|
13623
|
-
requires_approval: opts.approval !== undefined ? true : undefined
|
|
13781
|
+
requires_approval: opts.clearApproval ? false : opts.approval !== undefined ? true : undefined
|
|
13624
13782
|
});
|
|
13625
13783
|
} catch (e) {
|
|
13626
13784
|
handleError(e);
|
|
@@ -13637,7 +13795,14 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
|
|
|
13637
13795
|
const resolvedId = resolveTaskId(id);
|
|
13638
13796
|
const attachmentIds = opts.attachIds ? opts.attachIds.split(",").map((s) => s.trim()) : undefined;
|
|
13639
13797
|
const filesChanged = opts.filesChanged ? opts.filesChanged.split(",").map((s) => s.trim()) : undefined;
|
|
13640
|
-
|
|
13798
|
+
let confidence;
|
|
13799
|
+
if (opts.confidence !== undefined) {
|
|
13800
|
+
confidence = parseFloat(opts.confidence);
|
|
13801
|
+
if (!Number.isFinite(confidence) || confidence < 0 || confidence > 1) {
|
|
13802
|
+
console.error(chalk2.red("--confidence must be a number between 0.0 and 1.0"));
|
|
13803
|
+
process.exit(1);
|
|
13804
|
+
}
|
|
13805
|
+
}
|
|
13641
13806
|
const evidence = attachmentIds || filesChanged || opts.testResults || opts.commitHash || opts.notes ? { attachment_ids: attachmentIds, files_changed: filesChanged, test_results: opts.testResults, commit_hash: opts.commitHash, notes: opts.notes } : undefined;
|
|
13642
13807
|
let task2;
|
|
13643
13808
|
try {
|
|
@@ -23269,12 +23434,21 @@ function registerProjectCommands(program2) {
|
|
|
23269
23434
|
handleError(e);
|
|
23270
23435
|
}
|
|
23271
23436
|
});
|
|
23272
|
-
program2.command("comment <id> <text>").description("Add a comment to a task").action((id, text) => {
|
|
23437
|
+
program2.command("comment <id> <text>").alias("log-progress").description("Add a comment to a task (alias: log-progress, for recording intermediate progress)").option("--pct <percent>", "Progress percentage (0-100) to record alongside the note").action((id, text, opts) => {
|
|
23273
23438
|
const globalOpts = program2.opts();
|
|
23274
23439
|
const resolvedId = resolveTaskId(id);
|
|
23440
|
+
let content = text;
|
|
23441
|
+
if (opts.pct !== undefined) {
|
|
23442
|
+
const pct = parseInt(opts.pct, 10);
|
|
23443
|
+
if (!Number.isFinite(pct) || pct < 0 || pct > 100) {
|
|
23444
|
+
console.error(chalk4.red("--pct must be a number between 0 and 100"));
|
|
23445
|
+
process.exit(1);
|
|
23446
|
+
}
|
|
23447
|
+
content = `[progress ${pct}%] ${text}`;
|
|
23448
|
+
}
|
|
23275
23449
|
const comment = addComment({
|
|
23276
23450
|
task_id: resolvedId,
|
|
23277
|
-
content
|
|
23451
|
+
content,
|
|
23278
23452
|
agent_id: globalOpts.agent,
|
|
23279
23453
|
session_id: globalOpts.session
|
|
23280
23454
|
});
|
|
@@ -25406,10 +25580,8 @@ var init_token_utils = __esm(() => {
|
|
|
25406
25580
|
"cancel_task",
|
|
25407
25581
|
"check_task_done_contract",
|
|
25408
25582
|
"claim_task",
|
|
25409
|
-
"clone_task",
|
|
25410
25583
|
"delete_task",
|
|
25411
25584
|
"extend_task",
|
|
25412
|
-
"get_active_work",
|
|
25413
25585
|
"get_archived_tasks",
|
|
25414
25586
|
"get_blocked_tasks",
|
|
25415
25587
|
"get_blocking_tasks",
|
|
@@ -25445,7 +25617,8 @@ var init_token_utils = __esm(() => {
|
|
|
25445
25617
|
"task_context",
|
|
25446
25618
|
"unlock_task",
|
|
25447
25619
|
"unarchive_task",
|
|
25448
|
-
"update_task"
|
|
25620
|
+
"update_task",
|
|
25621
|
+
"upsert_task"
|
|
25449
25622
|
],
|
|
25450
25623
|
projects: [
|
|
25451
25624
|
"bootstrap_project",
|
|
@@ -25704,12 +25877,8 @@ var init_token_utils = __esm(() => {
|
|
|
25704
25877
|
"delete_tag",
|
|
25705
25878
|
"get_label",
|
|
25706
25879
|
"get_activity_timeline",
|
|
25707
|
-
"get_recent_activity",
|
|
25708
25880
|
"get_tag",
|
|
25709
25881
|
"get_task_fields",
|
|
25710
|
-
"get_task_graph",
|
|
25711
|
-
"get_task_history",
|
|
25712
|
-
"get_task_stats",
|
|
25713
25882
|
"list_workflow_states",
|
|
25714
25883
|
"list_labels",
|
|
25715
25884
|
"list_tags",
|
|
@@ -25720,6 +25889,10 @@ var init_token_utils = __esm(() => {
|
|
|
25720
25889
|
"describe_tools",
|
|
25721
25890
|
"set_task_workflow_state",
|
|
25722
25891
|
"set_task_fields",
|
|
25892
|
+
"assign_label_to_task",
|
|
25893
|
+
"create_custom_field",
|
|
25894
|
+
"set_task_custom_field",
|
|
25895
|
+
"set_task_priority_meta",
|
|
25723
25896
|
"update_label",
|
|
25724
25897
|
"update_tag"
|
|
25725
25898
|
],
|
|
@@ -25745,7 +25918,6 @@ var init_token_utils = __esm(() => {
|
|
|
25745
25918
|
"update_template",
|
|
25746
25919
|
"write_template_library"
|
|
25747
25920
|
],
|
|
25748
|
-
webhooks: ["create_webhook", "delete_webhook", "list_webhooks"],
|
|
25749
25921
|
machines: [
|
|
25750
25922
|
"machines_archive",
|
|
25751
25923
|
"machines_delete",
|
|
@@ -27303,6 +27475,11 @@ function safeEqualHex(a, b) {
|
|
|
27303
27475
|
return false;
|
|
27304
27476
|
return timingSafeEqual3(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
|
|
27305
27477
|
}
|
|
27478
|
+
function safeEqualStrings(a, b) {
|
|
27479
|
+
const ah = createHash6("sha256").update(a, "utf8").digest();
|
|
27480
|
+
const bh = createHash6("sha256").update(b, "utf8").digest();
|
|
27481
|
+
return timingSafeEqual3(ah, bh);
|
|
27482
|
+
}
|
|
27306
27483
|
function generatePlaintextKey() {
|
|
27307
27484
|
return `tdos_${randomBytes2(32).toString("base64url")}`;
|
|
27308
27485
|
}
|
|
@@ -27910,6 +28087,37 @@ function parseBoundedLimit(value, fallback, max) {
|
|
|
27910
28087
|
return fallback;
|
|
27911
28088
|
return Math.min(parsed, max);
|
|
27912
28089
|
}
|
|
28090
|
+
function mapTaskError(e, json2) {
|
|
28091
|
+
if (e instanceof VersionConflictError) {
|
|
28092
|
+
return json2({
|
|
28093
|
+
error: e.message,
|
|
28094
|
+
code: VersionConflictError.code,
|
|
28095
|
+
expected_version: e.expectedVersion,
|
|
28096
|
+
current_version: e.actualVersion
|
|
28097
|
+
}, 409);
|
|
28098
|
+
}
|
|
28099
|
+
if (e instanceof TaskNotFoundError) {
|
|
28100
|
+
return json2({ error: e.message, code: TaskNotFoundError.code }, 404);
|
|
28101
|
+
}
|
|
28102
|
+
if (e instanceof LockError) {
|
|
28103
|
+
return json2({ error: e.message, code: LockError.code }, 409);
|
|
28104
|
+
}
|
|
28105
|
+
if (e instanceof CompletionGuardError) {
|
|
28106
|
+
return json2({
|
|
28107
|
+
error: e.message,
|
|
28108
|
+
code: CompletionGuardError.code,
|
|
28109
|
+
retry_after: e.retryAfterSeconds ?? null
|
|
28110
|
+
}, 409);
|
|
28111
|
+
}
|
|
28112
|
+
if (e instanceof Error && (/ is blocked by /.test(e.message) || /cannot be started/.test(e.message))) {
|
|
28113
|
+
return json2({ error: e.message, code: "TASK_NOT_STARTABLE" }, 409);
|
|
28114
|
+
}
|
|
28115
|
+
return null;
|
|
28116
|
+
}
|
|
28117
|
+
function countRecurringTasks() {
|
|
28118
|
+
const row = getDatabase().query("SELECT COUNT(*) as count FROM tasks WHERE recurrence_rule IS NOT NULL AND recurrence_rule != '' AND archived_at IS NULL").get();
|
|
28119
|
+
return row?.count ?? 0;
|
|
28120
|
+
}
|
|
27913
28121
|
function handleSseEvents(_req, url, ctx) {
|
|
27914
28122
|
const agentId = url.searchParams.get("agent_id") || undefined;
|
|
27915
28123
|
const projectId = url.searchParams.get("project_id") || undefined;
|
|
@@ -27988,35 +28196,40 @@ data: ${JSON.stringify({ type: "connected", agent_id: agentId, timestamp: new Da
|
|
|
27988
28196
|
});
|
|
27989
28197
|
}
|
|
27990
28198
|
function handleHealth(_ctx, json2) {
|
|
27991
|
-
const
|
|
27992
|
-
const
|
|
27993
|
-
const
|
|
27994
|
-
return json2({
|
|
28199
|
+
const stats = getTaskStats();
|
|
28200
|
+
const staleCount = getStaleTasks(30).length;
|
|
28201
|
+
const overdueRecurring = getOverdueTasks().filter((t) => t.recurrence_rule && t.status === "pending").length;
|
|
28202
|
+
return json2({
|
|
28203
|
+
status: staleCount === 0 && overdueRecurring === 0 ? "ok" : "warn",
|
|
28204
|
+
tasks: stats.total,
|
|
28205
|
+
stale: staleCount,
|
|
28206
|
+
overdue_recurring: overdueRecurring,
|
|
28207
|
+
timestamp: new Date().toISOString()
|
|
28208
|
+
});
|
|
27995
28209
|
}
|
|
27996
28210
|
function handleHeadlessBoundary(_ctx, json2) {
|
|
27997
28211
|
const { getHeadlessBoundaryManifest: getHeadlessBoundaryManifest2 } = (init_headless_boundaries(), __toCommonJS(exports_headless_boundaries));
|
|
27998
28212
|
return json2(getHeadlessBoundaryManifest2());
|
|
27999
28213
|
}
|
|
28000
28214
|
function handleStats(_ctx, json2) {
|
|
28001
|
-
const
|
|
28215
|
+
const stats = getTaskStats();
|
|
28216
|
+
const byStatus = stats.by_status;
|
|
28002
28217
|
const projects = listProjects();
|
|
28003
28218
|
const agents = listAgents();
|
|
28004
|
-
const
|
|
28005
|
-
const
|
|
28006
|
-
const overdueRecurring = all.filter((t) => t.recurrence_rule && t.status === "pending" && t.due_at && t.due_at < nowStr).length;
|
|
28007
|
-
const recurringTasks = all.filter((t) => t.recurrence_rule).length;
|
|
28219
|
+
const staleCount = getStaleTasks(30).length;
|
|
28220
|
+
const overdueRecurring = getOverdueTasks().filter((t) => t.recurrence_rule && t.status === "pending").length;
|
|
28008
28221
|
return json2({
|
|
28009
|
-
total_tasks:
|
|
28010
|
-
pending:
|
|
28011
|
-
in_progress:
|
|
28012
|
-
completed:
|
|
28013
|
-
failed:
|
|
28014
|
-
cancelled:
|
|
28222
|
+
total_tasks: stats.total,
|
|
28223
|
+
pending: byStatus["pending"] ?? 0,
|
|
28224
|
+
in_progress: byStatus["in_progress"] ?? 0,
|
|
28225
|
+
completed: byStatus["completed"] ?? 0,
|
|
28226
|
+
failed: byStatus["failed"] ?? 0,
|
|
28227
|
+
cancelled: byStatus["cancelled"] ?? 0,
|
|
28015
28228
|
projects: projects.length,
|
|
28016
28229
|
agents: agents.length,
|
|
28017
|
-
stale_count:
|
|
28230
|
+
stale_count: staleCount,
|
|
28018
28231
|
overdue_recurring: overdueRecurring,
|
|
28019
|
-
recurring_tasks:
|
|
28232
|
+
recurring_tasks: countRecurringTasks()
|
|
28020
28233
|
});
|
|
28021
28234
|
}
|
|
28022
28235
|
async function handleListTasks(_req, url, _ctx, json2, taskToSummary2) {
|
|
@@ -28095,27 +28308,34 @@ function handleTasksExport(_req, url, _ctx, _json, taskToSummary2) {
|
|
|
28095
28308
|
const summaries = tasks.map((t) => taskToSummary2(t));
|
|
28096
28309
|
if (format === "csv") {
|
|
28097
28310
|
const headers = ["id", "short_id", "title", "status", "priority", "project_id", "assigned_to", "agent_id", "created_at", "updated_at", "completed_at", "due_at"];
|
|
28098
|
-
const
|
|
28099
|
-
const val = t[h];
|
|
28311
|
+
const csvCell = (val) => {
|
|
28100
28312
|
if (val === null || val === undefined)
|
|
28101
28313
|
return "";
|
|
28102
|
-
|
|
28103
|
-
|
|
28104
|
-
|
|
28105
|
-
|
|
28314
|
+
let str = String(val);
|
|
28315
|
+
if (/^[=+\-@\t\r]/.test(str))
|
|
28316
|
+
str = `'${str}`;
|
|
28317
|
+
if (str.includes(",") || str.includes('"') || str.includes(`
|
|
28318
|
+
`) || str.includes("\r")) {
|
|
28319
|
+
str = `"${str.replace(/"/g, '""')}"`;
|
|
28320
|
+
}
|
|
28321
|
+
return str;
|
|
28322
|
+
};
|
|
28323
|
+
const rows = summaries.map((t) => headers.map((h) => csvCell(t[h])).join(","));
|
|
28106
28324
|
const csv = [headers.join(","), ...rows].join(`
|
|
28107
28325
|
`);
|
|
28108
28326
|
return new Response(csv, {
|
|
28109
28327
|
headers: {
|
|
28110
28328
|
"Content-Type": "text/csv",
|
|
28111
|
-
"Content-Disposition": "attachment; filename=tasks.csv"
|
|
28329
|
+
"Content-Disposition": "attachment; filename=tasks.csv",
|
|
28330
|
+
...SECURITY_HEADERS
|
|
28112
28331
|
}
|
|
28113
28332
|
});
|
|
28114
28333
|
}
|
|
28115
28334
|
return new Response(JSON.stringify(summaries, null, 2), {
|
|
28116
28335
|
headers: {
|
|
28117
28336
|
"Content-Type": "application/json",
|
|
28118
|
-
"Content-Disposition": "attachment; filename=tasks.json"
|
|
28337
|
+
"Content-Disposition": "attachment; filename=tasks.json",
|
|
28338
|
+
...SECURITY_HEADERS
|
|
28119
28339
|
}
|
|
28120
28340
|
});
|
|
28121
28341
|
}
|
|
@@ -28289,12 +28509,16 @@ async function handlePatchTask(id, req, _ctx, json2, taskToSummary2) {
|
|
|
28289
28509
|
if (ALLOWED.has(key))
|
|
28290
28510
|
safeBody[key] = value;
|
|
28291
28511
|
}
|
|
28512
|
+
const clientVersion = typeof body["version"] === "number" ? body["version"] : task.version;
|
|
28292
28513
|
const updated = updateTask(id, {
|
|
28293
28514
|
...safeBody,
|
|
28294
|
-
version:
|
|
28515
|
+
version: clientVersion
|
|
28295
28516
|
});
|
|
28296
28517
|
return json2(taskToSummary2(updated));
|
|
28297
28518
|
} catch (e) {
|
|
28519
|
+
const mapped = mapTaskError(e, json2);
|
|
28520
|
+
if (mapped)
|
|
28521
|
+
return mapped;
|
|
28298
28522
|
return json2({ error: e instanceof Error ? e.message : "Failed to update task" }, 500);
|
|
28299
28523
|
}
|
|
28300
28524
|
}
|
|
@@ -28310,6 +28534,9 @@ function handleStartTask(id, ctx, json2, taskToSummary2) {
|
|
|
28310
28534
|
ctx.broadcastEvent({ type: "task", task_id: task.id, action: "started", agent_id: "dashboard", project_id: task.project_id });
|
|
28311
28535
|
return json2(taskToSummary2(task));
|
|
28312
28536
|
} catch (e) {
|
|
28537
|
+
const mapped = mapTaskError(e, json2);
|
|
28538
|
+
if (mapped)
|
|
28539
|
+
return mapped;
|
|
28313
28540
|
return json2({ error: e instanceof Error ? e.message : "Failed to start task" }, 500);
|
|
28314
28541
|
}
|
|
28315
28542
|
}
|
|
@@ -28329,6 +28556,9 @@ function handleCompleteTask(id, ctx, json2, taskToSummary2) {
|
|
|
28329
28556
|
ctx.broadcastEvent({ type: "task", task_id: task.id, action: "completed", agent_id: "dashboard", project_id: task.project_id });
|
|
28330
28557
|
return json2(taskToSummary2(task));
|
|
28331
28558
|
} catch (e) {
|
|
28559
|
+
const mapped = mapTaskError(e, json2);
|
|
28560
|
+
if (mapped)
|
|
28561
|
+
return mapped;
|
|
28332
28562
|
return json2({ error: e instanceof Error ? e.message : "Failed to complete task" }, 500);
|
|
28333
28563
|
}
|
|
28334
28564
|
}
|
|
@@ -28653,6 +28883,8 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
|
|
|
28653
28883
|
}
|
|
28654
28884
|
var init_routes = __esm(() => {
|
|
28655
28885
|
init_tasks();
|
|
28886
|
+
init_database();
|
|
28887
|
+
init_types();
|
|
28656
28888
|
init_projects();
|
|
28657
28889
|
init_agents();
|
|
28658
28890
|
init_plans();
|
|
@@ -51852,14 +52084,16 @@ function printHelp() {
|
|
|
51852
52084
|
Start the @hasna/todos MCP server.
|
|
51853
52085
|
|
|
51854
52086
|
Options:
|
|
51855
|
-
--stdio Use stdio transport
|
|
51856
|
-
--
|
|
52087
|
+
--stdio Use stdio transport (default)
|
|
52088
|
+
--http Use Streamable HTTP transport
|
|
52089
|
+
--port <port> Use Streamable HTTP on the given port (implies --http)
|
|
51857
52090
|
-V, --version output the version number
|
|
51858
52091
|
-h, --help display help for command
|
|
51859
52092
|
|
|
51860
52093
|
Environment:
|
|
51861
|
-
|
|
51862
|
-
|
|
52094
|
+
MCP_STDIO=1 Force stdio transport
|
|
52095
|
+
MCP_HTTP=1 Use Streamable HTTP transport
|
|
52096
|
+
MCP_HTTP_PORT=<port> HTTP port when using HTTP transport
|
|
51863
52097
|
TODOS_PROFILE=<profile> Tool profile filter
|
|
51864
52098
|
TODOS_TOOL_GROUPS=<list> Comma-separated tool group filter`);
|
|
51865
52099
|
}
|
|
@@ -51948,8 +52182,22 @@ function formatError(error) {
|
|
|
51948
52182
|
function resolveId(partialId, table = "tasks") {
|
|
51949
52183
|
const db = getDatabase();
|
|
51950
52184
|
const id = resolvePartialId(db, table, partialId);
|
|
51951
|
-
if (!id)
|
|
51952
|
-
|
|
52185
|
+
if (!id) {
|
|
52186
|
+
switch (table) {
|
|
52187
|
+
case "tasks":
|
|
52188
|
+
throw new TaskNotFoundError(partialId);
|
|
52189
|
+
case "projects":
|
|
52190
|
+
throw new ProjectNotFoundError(partialId);
|
|
52191
|
+
case "plans":
|
|
52192
|
+
throw new PlanNotFoundError(partialId);
|
|
52193
|
+
case "task_lists":
|
|
52194
|
+
throw new TaskListNotFoundError(partialId);
|
|
52195
|
+
case "agents":
|
|
52196
|
+
throw new AgentNotFoundError(partialId);
|
|
52197
|
+
default:
|
|
52198
|
+
throw new TaskNotFoundError(partialId);
|
|
52199
|
+
}
|
|
52200
|
+
}
|
|
51953
52201
|
return id;
|
|
51954
52202
|
}
|
|
51955
52203
|
function formatTask(task2) {
|
|
@@ -52038,8 +52286,9 @@ function buildServer() {
|
|
|
52038
52286
|
return server;
|
|
52039
52287
|
}
|
|
52040
52288
|
async function main() {
|
|
52041
|
-
const {
|
|
52042
|
-
|
|
52289
|
+
const { isHttpMode, resolveHttpPort } = await Promise.resolve().then(() => (init_http(), exports_http));
|
|
52290
|
+
const portRequested = process.argv.some((arg) => arg === "--port" || arg.startsWith("--port="));
|
|
52291
|
+
if (!isHttpMode() && !portRequested) {
|
|
52043
52292
|
const server = buildServer();
|
|
52044
52293
|
const transport = new StdioServerTransport;
|
|
52045
52294
|
await server.connect(transport);
|
|
@@ -52215,7 +52464,7 @@ function checkAuth(req, apiKey) {
|
|
|
52215
52464
|
if (!apiKey && !generatedKeysEnabled)
|
|
52216
52465
|
return null;
|
|
52217
52466
|
const provided = getProvidedApiKey(req);
|
|
52218
|
-
const matchesEnvKey = Boolean(apiKey && provided && provided
|
|
52467
|
+
const matchesEnvKey = Boolean(apiKey && provided && safeEqualStrings(provided, apiKey));
|
|
52219
52468
|
const matchesGeneratedKey = Boolean(provided && verifyApiKey(provided));
|
|
52220
52469
|
if (!matchesEnvKey && !matchesGeneratedKey) {
|
|
52221
52470
|
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
|
@@ -52225,6 +52474,15 @@ function checkAuth(req, apiKey) {
|
|
|
52225
52474
|
}
|
|
52226
52475
|
return null;
|
|
52227
52476
|
}
|
|
52477
|
+
function resolveClientIp(req, server) {
|
|
52478
|
+
const trustProxy = process.env["TODOS_TRUST_PROXY"] === "1" || process.env["TODOS_TRUST_PROXY"] === "true";
|
|
52479
|
+
if (trustProxy) {
|
|
52480
|
+
const forwarded = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip")?.trim();
|
|
52481
|
+
if (forwarded)
|
|
52482
|
+
return forwarded;
|
|
52483
|
+
}
|
|
52484
|
+
return server.requestIP(req)?.address || "unknown";
|
|
52485
|
+
}
|
|
52228
52486
|
function checkRateLimit(ip) {
|
|
52229
52487
|
const now4 = Date.now();
|
|
52230
52488
|
const entry = rateLimitMap.get(ip);
|
|
@@ -52352,7 +52610,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
52352
52610
|
const server = Bun.serve({
|
|
52353
52611
|
port,
|
|
52354
52612
|
hostname: hostname3,
|
|
52355
|
-
async fetch(req) {
|
|
52613
|
+
async fetch(req, server2) {
|
|
52356
52614
|
const url = new URL(req.url);
|
|
52357
52615
|
const path = url.pathname;
|
|
52358
52616
|
const method = req.method;
|
|
@@ -52364,15 +52622,6 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
52364
52622
|
Vary: "Origin"
|
|
52365
52623
|
} : undefined;
|
|
52366
52624
|
const jsonWithCors = (data, status = 200) => json(data, status, corsHeaders);
|
|
52367
|
-
if (path === "/health" && method === "GET") {
|
|
52368
|
-
const { healthResponse: healthResponse2 } = await Promise.resolve().then(() => (init_http(), exports_http));
|
|
52369
|
-
return healthResponse2("todos");
|
|
52370
|
-
}
|
|
52371
|
-
if (path === "/mcp") {
|
|
52372
|
-
const { handleMcpHttpRequest: handleMcpHttpRequest2 } = await Promise.resolve().then(() => (init_http(), exports_http));
|
|
52373
|
-
const { buildServer: buildServer2 } = await Promise.resolve().then(() => (init_mcp2(), exports_mcp));
|
|
52374
|
-
return handleMcpHttpRequest2(req, buildServer2);
|
|
52375
|
-
}
|
|
52376
52625
|
if (method === "OPTIONS") {
|
|
52377
52626
|
return new Response(null, {
|
|
52378
52627
|
headers: corsHeaders || {
|
|
@@ -52380,7 +52629,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
52380
52629
|
}
|
|
52381
52630
|
});
|
|
52382
52631
|
}
|
|
52383
|
-
const ip = req
|
|
52632
|
+
const ip = resolveClientIp(req, server2);
|
|
52384
52633
|
const rl = checkRateLimit(ip);
|
|
52385
52634
|
if (!rl.allowed) {
|
|
52386
52635
|
return new Response(JSON.stringify({ error: "Too many requests", retry_after: rl.retryAfter }), {
|
|
@@ -52388,6 +52637,18 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
52388
52637
|
headers: { "Content-Type": "application/json", "Retry-After": String(rl.retryAfter ?? 60), ...SECURITY_HEADERS }
|
|
52389
52638
|
});
|
|
52390
52639
|
}
|
|
52640
|
+
if (path === "/health" && method === "GET") {
|
|
52641
|
+
const { healthResponse: healthResponse2 } = await Promise.resolve().then(() => (init_http(), exports_http));
|
|
52642
|
+
return healthResponse2("todos");
|
|
52643
|
+
}
|
|
52644
|
+
if (path === "/mcp") {
|
|
52645
|
+
const authError = checkAuth(req, apiKey);
|
|
52646
|
+
if (authError)
|
|
52647
|
+
return authError;
|
|
52648
|
+
const { handleMcpHttpRequest: handleMcpHttpRequest2 } = await Promise.resolve().then(() => (init_http(), exports_http));
|
|
52649
|
+
const { buildServer: buildServer2 } = await Promise.resolve().then(() => (init_mcp2(), exports_mcp));
|
|
52650
|
+
return handleMcpHttpRequest2(req, buildServer2);
|
|
52651
|
+
}
|
|
52391
52652
|
if (path.startsWith("/api/")) {
|
|
52392
52653
|
const authError = checkAuth(req, apiKey);
|
|
52393
52654
|
if (authError)
|
|
@@ -56237,6 +56498,7 @@ function registerQueryCommands(program2) {
|
|
|
56237
56498
|
});
|
|
56238
56499
|
program2.command("next").description("Show the best pending task to work on next").option("--agent <id>", "Prefer tasks assigned to this agent").option("--project <id>", "Filter to project").option("-j, --json", "Output as JSON").action(async (opts) => {
|
|
56239
56500
|
const globalOpts = program2.opts();
|
|
56501
|
+
const json2 = opts.json || globalOpts.json;
|
|
56240
56502
|
const db = getDatabase();
|
|
56241
56503
|
const filters = {};
|
|
56242
56504
|
const projectInput = opts.project || globalOpts.project;
|
|
@@ -56247,10 +56509,14 @@ function registerQueryCommands(program2) {
|
|
|
56247
56509
|
}
|
|
56248
56510
|
const task2 = getNextTask(opts.agent, Object.keys(filters).length ? filters : undefined, db);
|
|
56249
56511
|
if (!task2) {
|
|
56512
|
+
if (json2) {
|
|
56513
|
+
console.log(JSON.stringify(null));
|
|
56514
|
+
return;
|
|
56515
|
+
}
|
|
56250
56516
|
console.log(chalk7.dim("No tasks available."));
|
|
56251
56517
|
return;
|
|
56252
56518
|
}
|
|
56253
|
-
if (
|
|
56519
|
+
if (json2) {
|
|
56254
56520
|
console.log(JSON.stringify(task2, null, 2));
|
|
56255
56521
|
return;
|
|
56256
56522
|
}
|
|
@@ -56260,16 +56526,26 @@ function registerQueryCommands(program2) {
|
|
|
56260
56526
|
console.log(chalk7.dim(` ${task2.description.slice(0, 100)}`));
|
|
56261
56527
|
});
|
|
56262
56528
|
program2.command("claim <agent>").description("Atomically claim the best pending task for an agent").option("--project <id>", "Filter to project").option("--steal-stale", "Steal the highest-priority stale task when no pending task is available").option("--stale-minutes <n>", "How long a task must be stale before stealing (default: 30)", "30").option("-j, --json", "Output as JSON").action(async (agent, opts) => {
|
|
56529
|
+
const globalOpts = program2.opts();
|
|
56530
|
+
const json2 = opts.json || globalOpts.json;
|
|
56263
56531
|
const db = getDatabase();
|
|
56264
56532
|
const filters = {};
|
|
56265
|
-
|
|
56266
|
-
|
|
56533
|
+
const projectInput = opts.project || globalOpts.project;
|
|
56534
|
+
if (projectInput) {
|
|
56535
|
+
const pid = autoProject({ project: projectInput }) || resolvePartialId(db, "projects", projectInput) || db.query("SELECT id FROM projects WHERE path = ? OR name = ? OR task_list_id = ?").get(projectInput, projectInput, projectInput)?.id;
|
|
56536
|
+
if (pid)
|
|
56537
|
+
filters.project_id = pid;
|
|
56538
|
+
}
|
|
56267
56539
|
const task2 = opts.stealStale ? (await Promise.resolve().then(() => (init_tasks(), exports_tasks))).claimOrSteal(agent, { ...filters, stale_minutes: parseInt(opts.staleMinutes, 10) }, db)?.task ?? null : claimNextTask(agent, Object.keys(filters).length ? filters : undefined, db);
|
|
56268
56540
|
if (!task2) {
|
|
56541
|
+
if (json2) {
|
|
56542
|
+
console.log(JSON.stringify(null));
|
|
56543
|
+
return;
|
|
56544
|
+
}
|
|
56269
56545
|
console.log(chalk7.dim("No tasks available to claim."));
|
|
56270
56546
|
return;
|
|
56271
56547
|
}
|
|
56272
|
-
if (
|
|
56548
|
+
if (json2) {
|
|
56273
56549
|
console.log(JSON.stringify(task2, null, 2));
|
|
56274
56550
|
return;
|
|
56275
56551
|
}
|
|
@@ -56290,12 +56566,14 @@ function registerQueryCommands(program2) {
|
|
|
56290
56566
|
console.log(chalk7.green(`Stolen: ${task2.short_id || task2.id.slice(0, 8)} | ${task2.priority} | ${task2.title}`));
|
|
56291
56567
|
});
|
|
56292
56568
|
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("-j, --json", "Output as JSON").action(async (opts) => {
|
|
56569
|
+
const globalOpts = program2.opts();
|
|
56570
|
+
const json2 = opts.json || globalOpts.json;
|
|
56293
56571
|
const db = getDatabase();
|
|
56294
56572
|
const filters = {};
|
|
56295
56573
|
if (opts.project)
|
|
56296
56574
|
filters.project_id = opts.project;
|
|
56297
56575
|
const s = getStatus(Object.keys(filters).length ? filters : undefined, opts.agent, undefined, db);
|
|
56298
|
-
if (
|
|
56576
|
+
if (json2) {
|
|
56299
56577
|
console.log(JSON.stringify(s, null, 2));
|
|
56300
56578
|
return;
|
|
56301
56579
|
}
|
|
@@ -56432,6 +56710,8 @@ Blocked:`));
|
|
|
56432
56710
|
console.log();
|
|
56433
56711
|
});
|
|
56434
56712
|
program2.command("fail <id>").description("Mark a task as failed with optional reason and retry").option("--reason <text>", "Why it failed").option("--agent <id>", "Agent reporting the failure").option("--retry", "Auto-create a retry copy").option("-j, --json", "Output as JSON").action(async (id, opts) => {
|
|
56713
|
+
const globalOpts = program2.opts();
|
|
56714
|
+
const json2 = opts.json || globalOpts.json;
|
|
56435
56715
|
const db = getDatabase();
|
|
56436
56716
|
const resolvedId = resolvePartialId(db, "tasks", id);
|
|
56437
56717
|
if (!resolvedId) {
|
|
@@ -56439,7 +56719,7 @@ Blocked:`));
|
|
|
56439
56719
|
process.exit(1);
|
|
56440
56720
|
}
|
|
56441
56721
|
const result = failTask(resolvedId, opts.agent, opts.reason, { retry: opts.retry }, db);
|
|
56442
|
-
if (
|
|
56722
|
+
if (json2) {
|
|
56443
56723
|
console.log(JSON.stringify(result, null, 2));
|
|
56444
56724
|
return;
|
|
56445
56725
|
}
|
|
@@ -56450,12 +56730,14 @@ Blocked:`));
|
|
|
56450
56730
|
console.log(chalk7.yellow(`Retry created: ${result.retryTask.short_id || result.retryTask.id.slice(0, 8)} | ${result.retryTask.title}`));
|
|
56451
56731
|
});
|
|
56452
56732
|
program2.command("active").description("Show all currently in-progress tasks").option("--project <id>", "Filter to project").option("-j, --json", "Output as JSON").action(async (opts) => {
|
|
56733
|
+
const globalOpts = program2.opts();
|
|
56734
|
+
const json2 = opts.json || globalOpts.json;
|
|
56453
56735
|
const db = getDatabase();
|
|
56454
56736
|
const filters = {};
|
|
56455
56737
|
if (opts.project)
|
|
56456
56738
|
filters.project_id = opts.project;
|
|
56457
56739
|
const work = getActiveWork(Object.keys(filters).length ? filters : undefined, db);
|
|
56458
|
-
if (
|
|
56740
|
+
if (json2) {
|
|
56459
56741
|
console.log(JSON.stringify(work, null, 2));
|
|
56460
56742
|
return;
|
|
56461
56743
|
}
|
|
@@ -56471,12 +56753,14 @@ Blocked:`));
|
|
|
56471
56753
|
}
|
|
56472
56754
|
});
|
|
56473
56755
|
program2.command("stale").description("Find tasks stuck in_progress with no recent activity").option("--minutes <n>", "Stale threshold in minutes", "30").option("--project <id>", "Filter to project").option("-j, --json", "Output as JSON").action(async (opts) => {
|
|
56756
|
+
const globalOpts = program2.opts();
|
|
56757
|
+
const json2 = opts.json || globalOpts.json;
|
|
56474
56758
|
const db = getDatabase();
|
|
56475
56759
|
const filters = {};
|
|
56476
56760
|
if (opts.project)
|
|
56477
56761
|
filters.project_id = opts.project;
|
|
56478
56762
|
const tasks = getStaleTasks(parseInt(opts.minutes, 10), Object.keys(filters).length ? filters : undefined, db);
|
|
56479
|
-
if (
|
|
56763
|
+
if (json2) {
|
|
56480
56764
|
console.log(JSON.stringify(tasks, null, 2));
|
|
56481
56765
|
return;
|
|
56482
56766
|
}
|
|
@@ -56500,7 +56784,7 @@ Blocked:`));
|
|
|
56500
56784
|
project_id: projectId,
|
|
56501
56785
|
limit: opts.limit ? parseInt(opts.limit, 10) : undefined
|
|
56502
56786
|
}, db);
|
|
56503
|
-
if (opts.json) {
|
|
56787
|
+
if (opts.json || globalOpts.json) {
|
|
56504
56788
|
console.log(JSON.stringify(result, null, 2));
|
|
56505
56789
|
return;
|
|
56506
56790
|
}
|
|
@@ -58653,7 +58937,7 @@ function envOption(value) {
|
|
|
58653
58937
|
}
|
|
58654
58938
|
function registerClaude(binPath, global) {
|
|
58655
58939
|
const scope = global ? "user" : "project";
|
|
58656
|
-
const cmd = `claude mcp add --transport stdio --scope ${scope} todos -- ${binPath}`;
|
|
58940
|
+
const cmd = `claude mcp add --transport stdio --scope ${scope} todos -- ${binPath} --stdio`;
|
|
58657
58941
|
try {
|
|
58658
58942
|
execSync3(cmd, { stdio: "pipe" });
|
|
58659
58943
|
console.log(chalk8.green(`Claude Code (${scope}): registered via 'claude mcp add'`));
|
|
@@ -58678,7 +58962,7 @@ function registerCodex(binPath) {
|
|
|
58678
58962
|
const block = `
|
|
58679
58963
|
[mcp_servers.todos]
|
|
58680
58964
|
command = "${binPath}"
|
|
58681
|
-
args = []
|
|
58965
|
+
args = ["--stdio"]
|
|
58682
58966
|
`;
|
|
58683
58967
|
content = content.trimEnd() + `
|
|
58684
58968
|
` + block;
|
|
@@ -58706,7 +58990,7 @@ function registerGemini(binPath) {
|
|
|
58706
58990
|
const servers = config["mcpServers"];
|
|
58707
58991
|
servers["todos"] = {
|
|
58708
58992
|
command: binPath,
|
|
58709
|
-
args: []
|
|
58993
|
+
args: ["--stdio"]
|
|
58710
58994
|
};
|
|
58711
58995
|
writeJsonFile2(configPath, config);
|
|
58712
58996
|
console.log(chalk8.green(`Gemini CLI: registered in ${configPath}`));
|
|
@@ -58837,7 +59121,11 @@ exit 0
|
|
|
58837
59121
|
unregisterMcp(opts.unregister, opts.global);
|
|
58838
59122
|
return;
|
|
58839
59123
|
}
|
|
58840
|
-
await Promise.resolve().then(() => (init_mcp2(), exports_mcp));
|
|
59124
|
+
const { buildServer: buildServer2 } = await Promise.resolve().then(() => (init_mcp2(), exports_mcp));
|
|
59125
|
+
const { StdioServerTransport: StdioServerTransport2 } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
59126
|
+
const server = buildServer2();
|
|
59127
|
+
const transport = new StdioServerTransport2;
|
|
59128
|
+
await server.connect(transport);
|
|
58841
59129
|
});
|
|
58842
59130
|
program2.command("import <url>").description("Import a GitHub issue as a task").option("--project <id>", "Project ID").option("--list <id>", "Task list ID").action(async (url, opts) => {
|
|
58843
59131
|
const globalOpts = program2.opts();
|
|
@@ -61229,7 +61517,6 @@ var init_cli_mcp_parity = __esm(() => {
|
|
|
61229
61517
|
"bulk_create_tasks",
|
|
61230
61518
|
"get_next_task",
|
|
61231
61519
|
"claim_next_task",
|
|
61232
|
-
"get_active_work",
|
|
61233
61520
|
"get_stale_tasks",
|
|
61234
61521
|
"get_my_tasks",
|
|
61235
61522
|
"get_blocked_tasks",
|
|
@@ -62261,11 +62548,8 @@ var init_cli_mcp_parity = __esm(() => {
|
|
|
62261
62548
|
"delete_search_view",
|
|
62262
62549
|
"get_status",
|
|
62263
62550
|
"standup",
|
|
62264
|
-
"get_task_stats",
|
|
62265
62551
|
"get_context",
|
|
62266
|
-
"task_context"
|
|
62267
|
-
"get_task_graph",
|
|
62268
|
-
"get_recent_activity"
|
|
62552
|
+
"task_context"
|
|
62269
62553
|
],
|
|
62270
62554
|
jsonContracts: ["task", "saved_search_view", "saved_search_run_result", "status_summary", "audit_history", "structured_error", "api_error"],
|
|
62271
62555
|
errorContracts: ["structured_error", "api_error"],
|
|
@@ -65778,7 +66062,7 @@ function upsertById(db, table, columns, row, updateClockColumn) {
|
|
|
65778
66062
|
const placeholders2 = presentColumns.map(() => "?").join(", ");
|
|
65779
66063
|
const values = presentColumns.map((column) => valueForColumn(column, row[column]));
|
|
65780
66064
|
const updateColumns = presentColumns.filter((column) => column !== "id");
|
|
65781
|
-
const updateSet = updateColumns.map((column) => `${column} = excluded.${column}`).join(", ");
|
|
66065
|
+
const updateSet = updateColumns.map((column) => column === "version" ? `version = MAX(COALESCE(${table}.version, 0), excluded.version)` : `${column} = excluded.${column}`).join(", ");
|
|
65782
66066
|
const clockGuard = updateClockColumn && presentColumns.includes(updateClockColumn) ? ` WHERE ${table}.${updateClockColumn} IS NULL OR ${table}.${updateClockColumn} <= excluded.${updateClockColumn}` : "";
|
|
65783
66067
|
const sql = updateSet ? `INSERT INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders2})
|
|
65784
66068
|
ON CONFLICT(id) DO UPDATE SET ${updateSet}${clockGuard}` : `INSERT OR IGNORE INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders2})`;
|
|
@@ -66222,7 +66506,9 @@ class PostgresTodosSyncStore {
|
|
|
66222
66506
|
deleted_at = EXCLUDED.deleted_at,
|
|
66223
66507
|
source_machine_id = EXCLUDED.source_machine_id,
|
|
66224
66508
|
version = EXCLUDED.version
|
|
66225
|
-
WHERE ${this.tableName}.updated_at
|
|
66509
|
+
WHERE ${this.tableName}.updated_at < EXCLUDED.updated_at
|
|
66510
|
+
OR (${this.tableName}.updated_at = EXCLUDED.updated_at
|
|
66511
|
+
AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))`, [
|
|
66226
66512
|
this.service,
|
|
66227
66513
|
entry.type,
|
|
66228
66514
|
entry.id,
|
|
@@ -66575,7 +66861,7 @@ class PostgresJsonRecordStore {
|
|
|
66575
66861
|
async upsert(type, value, context = {}) {
|
|
66576
66862
|
await this.ensureSchema();
|
|
66577
66863
|
const updatedAt = stringValue2(value.updated_at) ?? stringValue2(value.created_at) ?? new Date().toISOString();
|
|
66578
|
-
await this.options.client.query(`INSERT INTO ${this.tableName} (
|
|
66864
|
+
const result = await this.options.client.query(`INSERT INTO ${this.tableName} (
|
|
66579
66865
|
service, object_type, object_id, payload, updated_at,
|
|
66580
66866
|
deleted_at, source_machine_id, version
|
|
66581
66867
|
) VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, NULL, $6, $7)
|
|
@@ -66585,7 +66871,11 @@ class PostgresJsonRecordStore {
|
|
|
66585
66871
|
deleted_at = NULL,
|
|
66586
66872
|
source_machine_id = EXCLUDED.source_machine_id,
|
|
66587
66873
|
version = EXCLUDED.version
|
|
66588
|
-
WHERE ${this.tableName}.updated_at IS NULL
|
|
66874
|
+
WHERE ${this.tableName}.updated_at IS NULL
|
|
66875
|
+
OR ${this.tableName}.updated_at < EXCLUDED.updated_at
|
|
66876
|
+
OR (${this.tableName}.updated_at = EXCLUDED.updated_at
|
|
66877
|
+
AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))
|
|
66878
|
+
RETURNING object_id`, [
|
|
66589
66879
|
this.service,
|
|
66590
66880
|
type,
|
|
66591
66881
|
value.id,
|
|
@@ -66594,8 +66884,27 @@ class PostgresJsonRecordStore {
|
|
|
66594
66884
|
context.requestId ?? this.sourceMachineId ?? null,
|
|
66595
66885
|
numberValue3(value.version)
|
|
66596
66886
|
]);
|
|
66887
|
+
if (result.rows.length === 0) {
|
|
66888
|
+
const current = await this.get(type, value.id);
|
|
66889
|
+
if (current)
|
|
66890
|
+
return current;
|
|
66891
|
+
}
|
|
66597
66892
|
return value;
|
|
66598
66893
|
}
|
|
66894
|
+
async incrementProjectTaskCounter(projectId, _context = {}) {
|
|
66895
|
+
await this.ensureSchema();
|
|
66896
|
+
const result = await this.options.client.query(`UPDATE ${this.tableName}
|
|
66897
|
+
SET payload = jsonb_set(payload, '{task_counter}',
|
|
66898
|
+
to_jsonb(COALESCE((payload->>'task_counter')::bigint, 0) + 1)),
|
|
66899
|
+
updated_at = $3::timestamptz,
|
|
66900
|
+
version = COALESCE(version, 0) + 1
|
|
66901
|
+
WHERE service = $1 AND object_type = 'projects' AND object_id = $2 AND deleted_at IS NULL
|
|
66902
|
+
RETURNING payload->>'task_counter' AS counter`, [this.service, projectId, new Date().toISOString()]);
|
|
66903
|
+
const row = result.rows[0];
|
|
66904
|
+
if (!row || row.counter === null || row.counter === undefined)
|
|
66905
|
+
return null;
|
|
66906
|
+
return Number(row.counter);
|
|
66907
|
+
}
|
|
66599
66908
|
async delete(type, id, context = {}) {
|
|
66600
66909
|
await this.ensureSchema();
|
|
66601
66910
|
const existing = await this.get(type, id);
|
|
@@ -66751,6 +67060,9 @@ async function updateTask2(id, input, store) {
|
|
|
66751
67060
|
}
|
|
66752
67061
|
async function startTask2(id, agentId, store) {
|
|
66753
67062
|
const task2 = await requireRecord("tasks", id, store);
|
|
67063
|
+
if (task2.status !== "pending" && task2.status !== "in_progress") {
|
|
67064
|
+
throw new Error(`Task is ${task2.status} and cannot be started by ${agentId}`);
|
|
67065
|
+
}
|
|
66754
67066
|
return patchTask(task2, {
|
|
66755
67067
|
status: "in_progress",
|
|
66756
67068
|
assigned_to: task2.assigned_to ?? agentId,
|
|
@@ -66820,8 +67132,20 @@ async function getNextTask2(filters, store) {
|
|
|
66820
67132
|
return (await listTasks3({ ...filters, status: "pending", limit: 1 }, store))[0] ?? null;
|
|
66821
67133
|
}
|
|
66822
67134
|
async function claimNextTask2(agentId, filters, store) {
|
|
66823
|
-
const
|
|
66824
|
-
|
|
67135
|
+
const MAX_ATTEMPTS = 25;
|
|
67136
|
+
const tried = new Set;
|
|
67137
|
+
for (let attempt = 0;attempt < MAX_ATTEMPTS; attempt++) {
|
|
67138
|
+
const task2 = await getNextTask2(filters, store);
|
|
67139
|
+
if (!task2)
|
|
67140
|
+
return null;
|
|
67141
|
+
if (tried.has(task2.id))
|
|
67142
|
+
return null;
|
|
67143
|
+
tried.add(task2.id);
|
|
67144
|
+
try {
|
|
67145
|
+
return await startTask2(task2.id, agentId, store);
|
|
67146
|
+
} catch {}
|
|
67147
|
+
}
|
|
67148
|
+
return null;
|
|
66825
67149
|
}
|
|
66826
67150
|
async function getActiveWork2(filters, store) {
|
|
66827
67151
|
const tasks = await listTasks3({ ...filters, status: "in_progress" }, store);
|
|
@@ -67094,12 +67418,9 @@ async function nextTaskShortId2(projectId, store, context) {
|
|
|
67094
67418
|
const project = await store.get("projects", projectId);
|
|
67095
67419
|
if (!project?.task_prefix)
|
|
67096
67420
|
return null;
|
|
67097
|
-
const counter =
|
|
67098
|
-
|
|
67099
|
-
|
|
67100
|
-
task_counter: counter,
|
|
67101
|
-
updated_at: new Date().toISOString()
|
|
67102
|
-
}, context);
|
|
67421
|
+
const counter = await store.incrementProjectTaskCounter(projectId, context);
|
|
67422
|
+
if (counter === null)
|
|
67423
|
+
return null;
|
|
67103
67424
|
return `${project.task_prefix}-${String(counter).padStart(5, "0")}`;
|
|
67104
67425
|
}
|
|
67105
67426
|
async function generateProjectPrefix(name, store) {
|