@hasna/todos 0.11.71 → 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 +2162 -1450
- package/dist/cli-mcp-parity.d.ts.map +1 -1
- package/dist/contracts.js +6526 -6258
- 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.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10121 -9282
- package/dist/lib/db-backup.d.ts.map +1 -1
- package/dist/lib/shared-events.d.ts.map +1 -1
- package/dist/lib/task-route-contract.d.ts +2 -1
- package/dist/lib/task-route-contract.d.ts.map +1 -1
- package/dist/lib/task-route-sources.d.ts +68 -0
- package/dist/lib/task-route-sources.d.ts.map +1 -0
- package/dist/lib/task-routing.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1869 -1650
- package/dist/mcp/token-utils.d.ts.map +1 -1
- package/dist/mcp.js +6 -8
- package/dist/registry.js +6526 -6258
- 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 +1871 -1652
- 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 +3324 -3029
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -1011,7 +1011,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
|
|
|
1011
1011
|
this._exitCallback = (err) => {
|
|
1012
1012
|
if (err.code !== "commander.executeSubCommandAsync") {
|
|
1013
1013
|
throw err;
|
|
1014
|
-
}
|
|
1014
|
+
} else {}
|
|
1015
1015
|
};
|
|
1016
1016
|
}
|
|
1017
1017
|
return this;
|
|
@@ -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
|
}
|
|
@@ -7215,8 +7230,6 @@ function routeEnabledForTask(task, taskList) {
|
|
|
7215
7230
|
const explicit = booleanField(task.metadata.route_enabled);
|
|
7216
7231
|
if (explicit !== undefined)
|
|
7217
7232
|
return explicit;
|
|
7218
|
-
if (task.tags.includes("auto:route") || task.tags.includes("route:enabled"))
|
|
7219
|
-
return true;
|
|
7220
7233
|
const taskListDefault = taskList ? booleanField(taskList.metadata.route_enabled) : undefined;
|
|
7221
7234
|
if (taskListDefault !== undefined)
|
|
7222
7235
|
return taskListDefault;
|
|
@@ -7238,8 +7251,26 @@ function workflowPointersFromMetadata(metadata) {
|
|
|
7238
7251
|
function compactWorkflowPointers(pointers) {
|
|
7239
7252
|
return Object.fromEntries(Object.entries(pointers).filter(([, value]) => typeof value === "string" && value.length > 0));
|
|
7240
7253
|
}
|
|
7241
|
-
function
|
|
7242
|
-
|
|
7254
|
+
function metadataStringField(record, keys) {
|
|
7255
|
+
if (!record)
|
|
7256
|
+
return;
|
|
7257
|
+
for (const key of keys) {
|
|
7258
|
+
const value = record[key];
|
|
7259
|
+
if (typeof value === "string" && value.trim())
|
|
7260
|
+
return value.trim();
|
|
7261
|
+
}
|
|
7262
|
+
return;
|
|
7263
|
+
}
|
|
7264
|
+
function projectKindFromMetadata(...records) {
|
|
7265
|
+
for (const record of records) {
|
|
7266
|
+
const value = metadataStringField(record ?? undefined, ["project_kind", "projectKind", "source_kind", "sourceKind"]);
|
|
7267
|
+
if (value)
|
|
7268
|
+
return value;
|
|
7269
|
+
}
|
|
7270
|
+
return null;
|
|
7271
|
+
}
|
|
7272
|
+
function classifyProjectKind(_path, metadata) {
|
|
7273
|
+
return projectKindFromMetadata(metadata);
|
|
7243
7274
|
}
|
|
7244
7275
|
function isWorktreePath(path) {
|
|
7245
7276
|
return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
|
|
@@ -7312,7 +7343,6 @@ function taskEventMetadata(task) {
|
|
|
7312
7343
|
metadata.project_canonical_path = projectPath;
|
|
7313
7344
|
}
|
|
7314
7345
|
if (projectPath) {
|
|
7315
|
-
metadata.project_kind = classifyProjectKind(projectPath);
|
|
7316
7346
|
metadata.project_is_worktree = isWorktreePath(projectPath);
|
|
7317
7347
|
metadata.working_dir = task.working_dir ?? projectPath;
|
|
7318
7348
|
}
|
|
@@ -7324,6 +7354,10 @@ function taskEventMetadata(task) {
|
|
|
7324
7354
|
metadata.task_list_project_id = taskList.project_id;
|
|
7325
7355
|
metadata.task_list_is_project_default = Boolean(project?.task_list_id && taskList.slug === project.task_list_id);
|
|
7326
7356
|
}
|
|
7357
|
+
const projectKind = projectKindFromMetadata(task.metadata, taskList?.metadata);
|
|
7358
|
+
if (projectKind) {
|
|
7359
|
+
metadata.project_kind = classifyProjectKind(projectPath ?? "", { project_kind: projectKind });
|
|
7360
|
+
}
|
|
7327
7361
|
const routeEnabled = routeEnabledForTask(task, taskList);
|
|
7328
7362
|
if (routeEnabled !== undefined) {
|
|
7329
7363
|
metadata.route_enabled = routeEnabled;
|
|
@@ -7911,851 +7945,281 @@ var init_checklists = __esm(() => {
|
|
|
7911
7945
|
init_database();
|
|
7912
7946
|
});
|
|
7913
7947
|
|
|
7914
|
-
// src/
|
|
7915
|
-
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) {
|
|
7916
8068
|
return {
|
|
7917
8069
|
...row,
|
|
7918
8070
|
tags: JSON.parse(row.tags || "[]"),
|
|
8071
|
+
variables: JSON.parse(row.variables || "[]"),
|
|
7919
8072
|
metadata: JSON.parse(row.metadata || "{}"),
|
|
7920
|
-
|
|
7921
|
-
|
|
7922
|
-
requires_approval: !!row.requires_approval
|
|
8073
|
+
priority: row.priority || "medium",
|
|
8074
|
+
version: row.version ?? 1
|
|
7923
8075
|
};
|
|
7924
8076
|
}
|
|
7925
|
-
function
|
|
7926
|
-
|
|
7927
|
-
|
|
7928
|
-
|
|
7929
|
-
|
|
7930
|
-
|
|
7931
|
-
|
|
7932
|
-
|
|
7933
|
-
|
|
7934
|
-
|
|
7935
|
-
db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
|
|
7936
|
-
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
|
+
};
|
|
7937
8087
|
}
|
|
7938
|
-
function
|
|
7939
|
-
|
|
7940
|
-
return;
|
|
7941
|
-
for (const [key, value] of Object.entries(metadata)) {
|
|
7942
|
-
if (!/^[A-Za-z0-9_.-]+$/.test(key)) {
|
|
7943
|
-
throw new Error(`Invalid metadata filter key: ${key}`);
|
|
7944
|
-
}
|
|
7945
|
-
conditions.push(`json_extract(metadata, '$."${key}"') = ?`);
|
|
7946
|
-
params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
|
|
7947
|
-
}
|
|
8088
|
+
function resolveTemplateId(id, d) {
|
|
8089
|
+
return resolvePartialId(d, "task_templates", id);
|
|
7948
8090
|
}
|
|
7949
|
-
function
|
|
8091
|
+
function createTemplate(input, db) {
|
|
7950
8092
|
const d = db || getDatabase();
|
|
7951
|
-
const
|
|
7952
|
-
const tags = input.tags || [];
|
|
8093
|
+
const id = uuid();
|
|
7953
8094
|
const machineId = currentStorageMachineId(d);
|
|
7954
|
-
|
|
7955
|
-
|
|
7956
|
-
|
|
7957
|
-
|
|
7958
|
-
|
|
7959
|
-
|
|
7960
|
-
|
|
7961
|
-
|
|
7962
|
-
|
|
7963
|
-
|
|
7964
|
-
|
|
7965
|
-
|
|
7966
|
-
|
|
7967
|
-
|
|
7968
|
-
|
|
7969
|
-
|
|
7970
|
-
|
|
7971
|
-
input.priority || "medium",
|
|
7972
|
-
input.agent_id || null,
|
|
7973
|
-
input.assigned_to || null,
|
|
7974
|
-
input.session_id || null,
|
|
7975
|
-
input.working_dir || null,
|
|
7976
|
-
JSON.stringify(tags),
|
|
7977
|
-
JSON.stringify(input.metadata || {}),
|
|
7978
|
-
timestamp,
|
|
7979
|
-
timestamp,
|
|
7980
|
-
input.due_at || null,
|
|
7981
|
-
input.estimated_minutes || null,
|
|
7982
|
-
input.sla_minutes ?? null,
|
|
7983
|
-
input.confidence ?? null,
|
|
7984
|
-
input.retry_count ?? 0,
|
|
7985
|
-
input.max_retries ?? 3,
|
|
7986
|
-
input.retry_after ?? null,
|
|
7987
|
-
input.requires_approval ? 1 : 0,
|
|
7988
|
-
null,
|
|
7989
|
-
null,
|
|
7990
|
-
input.recurrence_rule || null,
|
|
7991
|
-
input.recurrence_parent_id || null,
|
|
7992
|
-
input.spawns_template_id || null,
|
|
7993
|
-
input.reason || null,
|
|
7994
|
-
input.spawned_from_session || null,
|
|
7995
|
-
assignedBy || null,
|
|
7996
|
-
assignedFromProject || null,
|
|
7997
|
-
input.task_type || null,
|
|
7998
|
-
machineId
|
|
7999
|
-
]);
|
|
8000
|
-
break;
|
|
8001
|
-
} catch (e) {
|
|
8002
|
-
if (attempt < 2 && e?.message?.includes("UNIQUE constraint failed: tasks.id")) {
|
|
8003
|
-
id = uuid();
|
|
8004
|
-
continue;
|
|
8005
|
-
}
|
|
8006
|
-
throw e;
|
|
8007
|
-
}
|
|
8008
|
-
}
|
|
8009
|
-
if (tags.length > 0) {
|
|
8010
|
-
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);
|
|
8011
8112
|
}
|
|
8012
|
-
|
|
8013
|
-
const payload = taskEventData(task);
|
|
8014
|
-
const databasePath = databasePathFromDatabase(d);
|
|
8015
|
-
dispatchWebhook2("task.created", payload, d).catch(() => {});
|
|
8016
|
-
emitLocalEventHooksQuiet({ type: "task.created", payload, databasePath });
|
|
8017
|
-
emitSharedTaskEventQuiet({ type: "task.created", task, databasePath });
|
|
8018
|
-
return task;
|
|
8113
|
+
return getTemplate(id, d);
|
|
8019
8114
|
}
|
|
8020
|
-
function
|
|
8115
|
+
function getTemplate(id, db) {
|
|
8021
8116
|
const d = db || getDatabase();
|
|
8022
|
-
const
|
|
8023
|
-
if (!
|
|
8117
|
+
const resolved = resolveTemplateId(id, d);
|
|
8118
|
+
if (!resolved)
|
|
8024
8119
|
return null;
|
|
8025
|
-
|
|
8120
|
+
const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(resolved);
|
|
8121
|
+
return row ? rowToTemplate(row) : null;
|
|
8026
8122
|
}
|
|
8027
|
-
function
|
|
8123
|
+
function listTemplates(db) {
|
|
8028
8124
|
const d = db || getDatabase();
|
|
8029
|
-
|
|
8030
|
-
if (!task)
|
|
8031
|
-
return null;
|
|
8032
|
-
const subtaskRows = d.query("SELECT * FROM tasks WHERE parent_id = ? ORDER BY created_at").all(id);
|
|
8033
|
-
const subtasks = subtaskRows.map(rowToTask);
|
|
8034
|
-
const depRows = d.query(`SELECT t.* FROM tasks t
|
|
8035
|
-
JOIN task_dependencies td ON td.depends_on = t.id
|
|
8036
|
-
WHERE td.task_id = ?`).all(id);
|
|
8037
|
-
const dependencies = depRows.map(rowToTask);
|
|
8038
|
-
const blockedByRows = d.query(`SELECT t.* FROM tasks t
|
|
8039
|
-
JOIN task_dependencies td ON td.task_id = t.id
|
|
8040
|
-
WHERE td.depends_on = ?`).all(id);
|
|
8041
|
-
const blocked_by = blockedByRows.map(rowToTask);
|
|
8042
|
-
const comments = d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(id);
|
|
8043
|
-
const parent = task.parent_id ? getTask(task.parent_id, d) : null;
|
|
8044
|
-
const checklist = getChecklist(id, d);
|
|
8045
|
-
return {
|
|
8046
|
-
...task,
|
|
8047
|
-
subtasks,
|
|
8048
|
-
dependencies,
|
|
8049
|
-
blocked_by,
|
|
8050
|
-
comments,
|
|
8051
|
-
parent,
|
|
8052
|
-
checklist
|
|
8053
|
-
};
|
|
8125
|
+
return d.query("SELECT * FROM task_templates ORDER BY name").all().map(rowToTemplate);
|
|
8054
8126
|
}
|
|
8055
|
-
function
|
|
8127
|
+
function deleteTemplate(id, db) {
|
|
8056
8128
|
const d = db || getDatabase();
|
|
8057
|
-
const
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
const
|
|
8061
|
-
if (
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
8065
|
-
|
|
8066
|
-
|
|
8067
|
-
|
|
8068
|
-
}
|
|
8069
|
-
|
|
8070
|
-
|
|
8071
|
-
|
|
8072
|
-
|
|
8073
|
-
|
|
8074
|
-
|
|
8075
|
-
|
|
8076
|
-
|
|
8077
|
-
if (
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
8088
|
-
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
conditions.push("priority = ?");
|
|
8092
|
-
params.push(filter.priority);
|
|
8093
|
-
}
|
|
8094
|
-
}
|
|
8095
|
-
if (filter.assigned_to) {
|
|
8096
|
-
conditions.push("assigned_to = ?");
|
|
8097
|
-
params.push(filter.assigned_to);
|
|
8098
|
-
}
|
|
8099
|
-
if (filter.agent_id) {
|
|
8100
|
-
conditions.push("agent_id = ?");
|
|
8101
|
-
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()]);
|
|
8102
8163
|
}
|
|
8103
|
-
|
|
8104
|
-
|
|
8105
|
-
|
|
8164
|
+
const sets = ["version = version + 1"];
|
|
8165
|
+
const values = [];
|
|
8166
|
+
if (updates.name !== undefined) {
|
|
8167
|
+
sets.push("name = ?");
|
|
8168
|
+
values.push(updates.name);
|
|
8106
8169
|
}
|
|
8107
|
-
if (
|
|
8108
|
-
|
|
8109
|
-
|
|
8110
|
-
params.push(...filter.tags);
|
|
8170
|
+
if (updates.title_pattern !== undefined) {
|
|
8171
|
+
sets.push("title_pattern = ?");
|
|
8172
|
+
values.push(updates.title_pattern);
|
|
8111
8173
|
}
|
|
8112
|
-
if (
|
|
8113
|
-
|
|
8114
|
-
|
|
8174
|
+
if (updates.description !== undefined) {
|
|
8175
|
+
sets.push("description = ?");
|
|
8176
|
+
values.push(updates.description);
|
|
8115
8177
|
}
|
|
8116
|
-
if (
|
|
8117
|
-
|
|
8118
|
-
|
|
8178
|
+
if (updates.priority !== undefined) {
|
|
8179
|
+
sets.push("priority = ?");
|
|
8180
|
+
values.push(updates.priority);
|
|
8119
8181
|
}
|
|
8120
|
-
if (
|
|
8121
|
-
|
|
8122
|
-
|
|
8123
|
-
conditions.push("recurrence_rule IS NULL");
|
|
8182
|
+
if (updates.tags !== undefined) {
|
|
8183
|
+
sets.push("tags = ?");
|
|
8184
|
+
values.push(JSON.stringify(updates.tags));
|
|
8124
8185
|
}
|
|
8125
|
-
if (
|
|
8126
|
-
|
|
8127
|
-
|
|
8128
|
-
params.push(...filter.task_type);
|
|
8129
|
-
} else {
|
|
8130
|
-
conditions.push("task_type = ?");
|
|
8131
|
-
params.push(filter.task_type);
|
|
8132
|
-
}
|
|
8186
|
+
if (updates.variables !== undefined) {
|
|
8187
|
+
sets.push("variables = ?");
|
|
8188
|
+
values.push(JSON.stringify(updates.variables));
|
|
8133
8189
|
}
|
|
8134
|
-
|
|
8135
|
-
|
|
8136
|
-
|
|
8137
|
-
try {
|
|
8138
|
-
const decoded = JSON.parse(Buffer.from(filter.cursor, "base64").toString("utf8"));
|
|
8139
|
-
conditions.push(`(${PRIORITY_RANK} > ? OR (${PRIORITY_RANK} = ? AND created_at < ?) OR (${PRIORITY_RANK} = ? AND created_at = ? AND id > ?))`);
|
|
8140
|
-
params.push(decoded.p, decoded.p, decoded.c, decoded.p, decoded.c, decoded.i);
|
|
8141
|
-
} catch {}
|
|
8190
|
+
if (updates.project_id !== undefined) {
|
|
8191
|
+
sets.push("project_id = ?");
|
|
8192
|
+
values.push(updates.project_id);
|
|
8142
8193
|
}
|
|
8143
|
-
if (
|
|
8144
|
-
|
|
8194
|
+
if (updates.plan_id !== undefined) {
|
|
8195
|
+
sets.push("plan_id = ?");
|
|
8196
|
+
values.push(updates.plan_id);
|
|
8145
8197
|
}
|
|
8146
|
-
|
|
8147
|
-
|
|
8148
|
-
|
|
8149
|
-
limitClause = " LIMIT ?";
|
|
8150
|
-
params.push(filter.limit);
|
|
8151
|
-
if (!filter.cursor && filter.offset) {
|
|
8152
|
-
limitClause += " OFFSET ?";
|
|
8153
|
-
params.push(filter.offset);
|
|
8154
|
-
}
|
|
8198
|
+
if (updates.metadata !== undefined) {
|
|
8199
|
+
sets.push("metadata = ?");
|
|
8200
|
+
values.push(JSON.stringify(updates.metadata));
|
|
8155
8201
|
}
|
|
8156
|
-
|
|
8157
|
-
|
|
8158
|
-
|
|
8159
|
-
function getTaskByFingerprint(fingerprint, db) {
|
|
8160
|
-
const tasks = listTasks({ metadata: { fingerprint }, limit: 1 }, db);
|
|
8161
|
-
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);
|
|
8162
8205
|
}
|
|
8163
|
-
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));
|
|
8164
8211
|
return {
|
|
8165
|
-
|
|
8166
|
-
|
|
8167
|
-
|
|
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
|
|
8168
8220
|
};
|
|
8169
8221
|
}
|
|
8170
|
-
function
|
|
8171
|
-
const d = db || getDatabase();
|
|
8172
|
-
const fingerprint = input.fingerprint.trim();
|
|
8173
|
-
if (!fingerprint)
|
|
8174
|
-
throw new Error("fingerprint is required");
|
|
8175
|
-
const existing = getTaskByFingerprint(fingerprint, d);
|
|
8176
|
-
const metadata = mergeTaskMetadata(existing?.metadata ?? {}, input.metadata, fingerprint);
|
|
8177
|
-
if (!existing) {
|
|
8178
|
-
const task2 = createTask({ ...input, metadata }, d);
|
|
8179
|
-
return { task: task2, created: true };
|
|
8180
|
-
}
|
|
8181
|
-
const task = updateTask(existing.id, {
|
|
8182
|
-
version: existing.version,
|
|
8183
|
-
title: input.title,
|
|
8184
|
-
description: input.description,
|
|
8185
|
-
status: input.status,
|
|
8186
|
-
priority: input.priority,
|
|
8187
|
-
project_id: input.project_id,
|
|
8188
|
-
assigned_to: input.assigned_to,
|
|
8189
|
-
working_dir: input.working_dir,
|
|
8190
|
-
plan_id: input.plan_id,
|
|
8191
|
-
task_list_id: input.task_list_id,
|
|
8192
|
-
tags: input.tags,
|
|
8193
|
-
metadata,
|
|
8194
|
-
due_at: input.due_at,
|
|
8195
|
-
estimated_minutes: input.estimated_minutes,
|
|
8196
|
-
sla_minutes: input.sla_minutes,
|
|
8197
|
-
confidence: input.confidence,
|
|
8198
|
-
retry_count: input.retry_count,
|
|
8199
|
-
max_retries: input.max_retries,
|
|
8200
|
-
retry_after: input.retry_after,
|
|
8201
|
-
requires_approval: input.requires_approval,
|
|
8202
|
-
recurrence_rule: input.recurrence_rule,
|
|
8203
|
-
task_type: input.task_type
|
|
8204
|
-
}, d);
|
|
8205
|
-
return { task, created: false };
|
|
8206
|
-
}
|
|
8207
|
-
function countTasks(filter = {}, db) {
|
|
8208
|
-
const d = db || getDatabase();
|
|
8209
|
-
const conditions = [];
|
|
8210
|
-
const params = [];
|
|
8211
|
-
if (filter.project_id) {
|
|
8212
|
-
conditions.push("project_id = ?");
|
|
8213
|
-
params.push(filter.project_id);
|
|
8214
|
-
}
|
|
8215
|
-
if (filter.ids && filter.ids.length > 0) {
|
|
8216
|
-
conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
|
|
8217
|
-
params.push(...filter.ids);
|
|
8218
|
-
}
|
|
8219
|
-
if (filter.parent_id !== undefined) {
|
|
8220
|
-
if (filter.parent_id === null) {
|
|
8221
|
-
conditions.push("parent_id IS NULL");
|
|
8222
|
-
} else {
|
|
8223
|
-
conditions.push("parent_id = ?");
|
|
8224
|
-
params.push(filter.parent_id);
|
|
8225
|
-
}
|
|
8226
|
-
}
|
|
8227
|
-
if (filter.status) {
|
|
8228
|
-
if (Array.isArray(filter.status)) {
|
|
8229
|
-
conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
|
|
8230
|
-
params.push(...filter.status);
|
|
8231
|
-
} else {
|
|
8232
|
-
conditions.push("status = ?");
|
|
8233
|
-
params.push(filter.status);
|
|
8234
|
-
}
|
|
8235
|
-
}
|
|
8236
|
-
if (filter.priority) {
|
|
8237
|
-
if (Array.isArray(filter.priority)) {
|
|
8238
|
-
conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
|
|
8239
|
-
params.push(...filter.priority);
|
|
8240
|
-
} else {
|
|
8241
|
-
conditions.push("priority = ?");
|
|
8242
|
-
params.push(filter.priority);
|
|
8243
|
-
}
|
|
8244
|
-
}
|
|
8245
|
-
if (filter.assigned_to) {
|
|
8246
|
-
conditions.push("assigned_to = ?");
|
|
8247
|
-
params.push(filter.assigned_to);
|
|
8248
|
-
}
|
|
8249
|
-
if (filter.agent_id) {
|
|
8250
|
-
conditions.push("agent_id = ?");
|
|
8251
|
-
params.push(filter.agent_id);
|
|
8252
|
-
}
|
|
8253
|
-
if (filter.session_id) {
|
|
8254
|
-
conditions.push("session_id = ?");
|
|
8255
|
-
params.push(filter.session_id);
|
|
8256
|
-
}
|
|
8257
|
-
if (filter.tags && filter.tags.length > 0) {
|
|
8258
|
-
const placeholders = filter.tags.map(() => "?").join(",");
|
|
8259
|
-
conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
|
|
8260
|
-
params.push(...filter.tags);
|
|
8261
|
-
}
|
|
8262
|
-
if (filter.plan_id) {
|
|
8263
|
-
conditions.push("plan_id = ?");
|
|
8264
|
-
params.push(filter.plan_id);
|
|
8265
|
-
}
|
|
8266
|
-
if (filter.task_list_id) {
|
|
8267
|
-
conditions.push("task_list_id = ?");
|
|
8268
|
-
params.push(filter.task_list_id);
|
|
8269
|
-
}
|
|
8270
|
-
addMetadataConditions(filter.metadata, conditions, params);
|
|
8271
|
-
if (!filter.include_archived) {
|
|
8272
|
-
conditions.push("archived_at IS NULL");
|
|
8273
|
-
}
|
|
8274
|
-
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
8275
|
-
const row = d.query(`SELECT COUNT(*) as count FROM tasks ${where}`).get(...params);
|
|
8276
|
-
return row.count;
|
|
8277
|
-
}
|
|
8278
|
-
function updateTask(id, input, db) {
|
|
8279
|
-
const d = db || getDatabase();
|
|
8280
|
-
const task = getTask(id, d);
|
|
8281
|
-
if (!task)
|
|
8282
|
-
throw new TaskNotFoundError(id);
|
|
8283
|
-
if (task.version !== input.version) {
|
|
8284
|
-
throw new VersionConflictError(id, input.version, task.version);
|
|
8285
|
-
}
|
|
8286
|
-
const timestamp = now();
|
|
8287
|
-
const completionTimestamp = input.completed_at ?? timestamp;
|
|
8288
|
-
const sets = ["version = version + 1", "updated_at = ?"];
|
|
8289
|
-
const params = [timestamp];
|
|
8290
|
-
if (input.title !== undefined) {
|
|
8291
|
-
sets.push("title = ?");
|
|
8292
|
-
params.push(input.title);
|
|
8293
|
-
}
|
|
8294
|
-
if (input.description !== undefined) {
|
|
8295
|
-
sets.push("description = ?");
|
|
8296
|
-
params.push(input.description);
|
|
8297
|
-
}
|
|
8298
|
-
if (input.status !== undefined) {
|
|
8299
|
-
if (input.status === "completed") {
|
|
8300
|
-
checkCompletionGuard(task, task.assigned_to || task.agent_id || null, d);
|
|
8301
|
-
}
|
|
8302
|
-
sets.push("status = ?");
|
|
8303
|
-
params.push(input.status);
|
|
8304
|
-
if (input.status === "completed") {
|
|
8305
|
-
sets.push("completed_at = ?");
|
|
8306
|
-
params.push(completionTimestamp);
|
|
8307
|
-
}
|
|
8308
|
-
}
|
|
8309
|
-
if (input.priority !== undefined) {
|
|
8310
|
-
sets.push("priority = ?");
|
|
8311
|
-
params.push(input.priority);
|
|
8312
|
-
}
|
|
8313
|
-
if (input.project_id !== undefined) {
|
|
8314
|
-
sets.push("project_id = ?");
|
|
8315
|
-
params.push(input.project_id);
|
|
8316
|
-
}
|
|
8317
|
-
if (input.assigned_to !== undefined) {
|
|
8318
|
-
sets.push("assigned_to = ?");
|
|
8319
|
-
params.push(input.assigned_to);
|
|
8320
|
-
}
|
|
8321
|
-
if (input.working_dir !== undefined) {
|
|
8322
|
-
sets.push("working_dir = ?");
|
|
8323
|
-
params.push(input.working_dir);
|
|
8324
|
-
}
|
|
8325
|
-
if (input.tags !== undefined) {
|
|
8326
|
-
sets.push("tags = ?");
|
|
8327
|
-
params.push(JSON.stringify(input.tags));
|
|
8328
|
-
}
|
|
8329
|
-
if (input.metadata !== undefined) {
|
|
8330
|
-
sets.push("metadata = ?");
|
|
8331
|
-
params.push(JSON.stringify(input.metadata));
|
|
8332
|
-
}
|
|
8333
|
-
if (input.plan_id !== undefined) {
|
|
8334
|
-
sets.push("plan_id = ?");
|
|
8335
|
-
params.push(input.plan_id);
|
|
8336
|
-
}
|
|
8337
|
-
if (input.task_list_id !== undefined) {
|
|
8338
|
-
sets.push("task_list_id = ?");
|
|
8339
|
-
params.push(input.task_list_id);
|
|
8340
|
-
}
|
|
8341
|
-
if (input.due_at !== undefined) {
|
|
8342
|
-
sets.push("due_at = ?");
|
|
8343
|
-
params.push(input.due_at);
|
|
8344
|
-
}
|
|
8345
|
-
if (input.estimated_minutes !== undefined) {
|
|
8346
|
-
sets.push("estimated_minutes = ?");
|
|
8347
|
-
params.push(input.estimated_minutes);
|
|
8348
|
-
}
|
|
8349
|
-
if (input.sla_minutes !== undefined) {
|
|
8350
|
-
sets.push("sla_minutes = ?");
|
|
8351
|
-
params.push(input.sla_minutes);
|
|
8352
|
-
}
|
|
8353
|
-
if (input.actual_minutes !== undefined) {
|
|
8354
|
-
sets.push("actual_minutes = ?");
|
|
8355
|
-
params.push(input.actual_minutes);
|
|
8356
|
-
}
|
|
8357
|
-
if (input.completed_at !== undefined && input.status !== "completed") {
|
|
8358
|
-
sets.push("completed_at = ?");
|
|
8359
|
-
params.push(input.completed_at);
|
|
8360
|
-
}
|
|
8361
|
-
if (input.confidence !== undefined) {
|
|
8362
|
-
sets.push("confidence = ?");
|
|
8363
|
-
params.push(input.confidence);
|
|
8364
|
-
}
|
|
8365
|
-
if (input.retry_count !== undefined) {
|
|
8366
|
-
sets.push("retry_count = ?");
|
|
8367
|
-
params.push(input.retry_count);
|
|
8368
|
-
}
|
|
8369
|
-
if (input.max_retries !== undefined) {
|
|
8370
|
-
sets.push("max_retries = ?");
|
|
8371
|
-
params.push(input.max_retries);
|
|
8372
|
-
}
|
|
8373
|
-
if (input.retry_after !== undefined) {
|
|
8374
|
-
sets.push("retry_after = ?");
|
|
8375
|
-
params.push(input.retry_after);
|
|
8376
|
-
}
|
|
8377
|
-
if (input.requires_approval !== undefined) {
|
|
8378
|
-
sets.push("requires_approval = ?");
|
|
8379
|
-
params.push(input.requires_approval ? 1 : 0);
|
|
8380
|
-
}
|
|
8381
|
-
if (input.approved_by !== undefined) {
|
|
8382
|
-
sets.push("approved_by = ?");
|
|
8383
|
-
params.push(input.approved_by);
|
|
8384
|
-
sets.push("approved_at = ?");
|
|
8385
|
-
params.push(now());
|
|
8386
|
-
}
|
|
8387
|
-
if (input.recurrence_rule !== undefined) {
|
|
8388
|
-
sets.push("recurrence_rule = ?");
|
|
8389
|
-
params.push(input.recurrence_rule);
|
|
8390
|
-
}
|
|
8391
|
-
if (input.task_type !== undefined) {
|
|
8392
|
-
sets.push("task_type = ?");
|
|
8393
|
-
params.push(input.task_type ?? null);
|
|
8394
|
-
}
|
|
8395
|
-
params.push(id, input.version);
|
|
8396
|
-
const result = d.run(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ? AND version = ?`, params);
|
|
8397
|
-
if (result.changes === 0) {
|
|
8398
|
-
const current = getTask(id, d);
|
|
8399
|
-
throw new VersionConflictError(id, input.version, current?.version ?? -1);
|
|
8400
|
-
}
|
|
8401
|
-
if (input.tags !== undefined) {
|
|
8402
|
-
replaceTaskTags(id, input.tags, d);
|
|
8403
|
-
}
|
|
8404
|
-
const agentId = task.assigned_to || task.agent_id || null;
|
|
8405
|
-
if (input.status !== undefined && input.status !== task.status)
|
|
8406
|
-
logTaskChange(id, "update", "status", task.status, input.status, agentId, d);
|
|
8407
|
-
if (input.priority !== undefined && input.priority !== task.priority)
|
|
8408
|
-
logTaskChange(id, "update", "priority", task.priority, input.priority, agentId, d);
|
|
8409
|
-
if (input.title !== undefined && input.title !== task.title)
|
|
8410
|
-
logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
|
|
8411
|
-
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
|
|
8412
|
-
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
8413
|
-
if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
|
|
8414
|
-
logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
|
|
8415
|
-
if (input.approved_by !== undefined)
|
|
8416
|
-
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
8417
|
-
const updatedTask = {
|
|
8418
|
-
...task,
|
|
8419
|
-
...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
|
|
8420
|
-
tags: input.tags ?? task.tags,
|
|
8421
|
-
metadata: input.metadata ?? task.metadata,
|
|
8422
|
-
version: task.version + 1,
|
|
8423
|
-
updated_at: timestamp,
|
|
8424
|
-
completed_at: input.status === "completed" ? completionTimestamp : input.completed_at !== undefined ? input.completed_at : task.completed_at,
|
|
8425
|
-
sla_minutes: input.sla_minutes !== undefined ? input.sla_minutes : task.sla_minutes,
|
|
8426
|
-
actual_minutes: input.actual_minutes ?? task.actual_minutes,
|
|
8427
|
-
confidence: input.confidence !== undefined ? input.confidence : task.confidence,
|
|
8428
|
-
retry_count: input.retry_count ?? task.retry_count,
|
|
8429
|
-
max_retries: input.max_retries ?? task.max_retries,
|
|
8430
|
-
retry_after: input.retry_after !== undefined ? input.retry_after : task.retry_after,
|
|
8431
|
-
requires_approval: input.requires_approval !== undefined ? input.requires_approval : task.requires_approval,
|
|
8432
|
-
approved_by: input.approved_by ?? task.approved_by,
|
|
8433
|
-
approved_at: input.approved_by ? timestamp : task.approved_at
|
|
8434
|
-
};
|
|
8435
|
-
const databasePath = databasePathFromDatabase(d);
|
|
8436
|
-
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
|
|
8437
|
-
const payload = taskEventData(updatedTask, { assigned_to: input.assigned_to, old_assigned_to: task.assigned_to });
|
|
8438
|
-
dispatchWebhook2("task.assigned", payload, d).catch(() => {});
|
|
8439
|
-
emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
|
|
8440
|
-
emitSharedTaskEventQuiet({ type: "task.assigned", task: updatedTask, data: { old_assigned_to: task.assigned_to }, databasePath });
|
|
8441
|
-
}
|
|
8442
|
-
if (input.status !== undefined && input.status !== task.status) {
|
|
8443
|
-
const payload = taskEventData(updatedTask, { old_status: task.status, new_status: input.status });
|
|
8444
|
-
dispatchWebhook2("task.status_changed", payload, d).catch(() => {});
|
|
8445
|
-
emitLocalEventHooksQuiet({ type: "task.status_changed", payload, databasePath });
|
|
8446
|
-
emitSharedTaskEventQuiet({ type: "task.status_changed", task: updatedTask, data: { old_status: task.status, new_status: input.status }, databasePath });
|
|
8447
|
-
}
|
|
8448
|
-
if (input.approved_by !== undefined) {
|
|
8449
|
-
emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title }, databasePath });
|
|
8450
|
-
}
|
|
8451
|
-
const updatePayload = taskEventData(updatedTask);
|
|
8452
|
-
dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
|
|
8453
|
-
emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload, databasePath });
|
|
8454
|
-
emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask, databasePath });
|
|
8455
|
-
return updatedTask;
|
|
8456
|
-
}
|
|
8457
|
-
function deleteTask(id, db) {
|
|
8458
|
-
const d = db || getDatabase();
|
|
8459
|
-
const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
|
|
8460
|
-
if (!row)
|
|
8461
|
-
return false;
|
|
8462
|
-
recordStorageTombstone({
|
|
8463
|
-
object_type: "tasks",
|
|
8464
|
-
object_id: id,
|
|
8465
|
-
payload: rowToTask(row),
|
|
8466
|
-
version: row.version
|
|
8467
|
-
}, d);
|
|
8468
|
-
const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
|
|
8469
|
-
return result.changes > 0;
|
|
8470
|
-
}
|
|
8471
|
-
var init_task_crud = __esm(() => {
|
|
8472
|
-
init_types();
|
|
8473
|
-
init_database();
|
|
8474
|
-
init_completion_guard();
|
|
8475
|
-
init_event_emission_safety();
|
|
8476
|
-
init_event_hooks();
|
|
8477
|
-
init_shared_events();
|
|
8478
|
-
init_audit();
|
|
8479
|
-
init_webhooks();
|
|
8480
|
-
init_checklists();
|
|
8481
|
-
init_storage_tombstones();
|
|
8482
|
-
});
|
|
8483
|
-
|
|
8484
|
-
// src/lib/recurrence.ts
|
|
8485
|
-
function parseRecurrenceRule(rule) {
|
|
8486
|
-
const normalized = rule.trim().toLowerCase();
|
|
8487
|
-
if (normalized === "every weekday" || normalized === "every weekdays") {
|
|
8488
|
-
return { type: "specific_days", days: [1, 2, 3, 4, 5] };
|
|
8489
|
-
}
|
|
8490
|
-
if (normalized === "every day" || normalized === "daily") {
|
|
8491
|
-
return { type: "interval", interval: 1, unit: "day" };
|
|
8492
|
-
}
|
|
8493
|
-
if (normalized === "every week" || normalized === "weekly") {
|
|
8494
|
-
return { type: "interval", interval: 1, unit: "week" };
|
|
8495
|
-
}
|
|
8496
|
-
if (normalized === "every month" || normalized === "monthly") {
|
|
8497
|
-
return { type: "interval", interval: 1, unit: "month" };
|
|
8498
|
-
}
|
|
8499
|
-
const intervalMatch = normalized.match(/^every\s+(\d+)\s+(day|week|month)s?$/);
|
|
8500
|
-
if (intervalMatch) {
|
|
8501
|
-
return {
|
|
8502
|
-
type: "interval",
|
|
8503
|
-
interval: parseInt(intervalMatch[1], 10),
|
|
8504
|
-
unit: intervalMatch[2]
|
|
8505
|
-
};
|
|
8506
|
-
}
|
|
8507
|
-
const daysMatch = normalized.match(/^every\s+(.+)$/);
|
|
8508
|
-
if (daysMatch) {
|
|
8509
|
-
const dayParts = daysMatch[1].split(/[,\s]+/).map((d) => d.trim()).filter(Boolean);
|
|
8510
|
-
const days = [];
|
|
8511
|
-
for (const part of dayParts) {
|
|
8512
|
-
const dayNum = DAY_NAMES[part];
|
|
8513
|
-
if (dayNum !== undefined) {
|
|
8514
|
-
days.push(dayNum);
|
|
8515
|
-
}
|
|
8516
|
-
}
|
|
8517
|
-
if (days.length > 0) {
|
|
8518
|
-
return { type: "specific_days", days: days.sort((a, b) => a - b) };
|
|
8519
|
-
}
|
|
8520
|
-
}
|
|
8521
|
-
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"`);
|
|
8522
|
-
}
|
|
8523
|
-
function isValidRecurrenceRule(rule) {
|
|
8524
|
-
try {
|
|
8525
|
-
parseRecurrenceRule(rule);
|
|
8526
|
-
return true;
|
|
8527
|
-
} catch {
|
|
8528
|
-
return false;
|
|
8529
|
-
}
|
|
8530
|
-
}
|
|
8531
|
-
function nextOccurrence(rule, from) {
|
|
8532
|
-
const parsed = parseRecurrenceRule(rule);
|
|
8533
|
-
const base = from || new Date;
|
|
8534
|
-
if (parsed.type === "interval") {
|
|
8535
|
-
const next = new Date(base);
|
|
8536
|
-
if (parsed.unit === "day") {
|
|
8537
|
-
next.setDate(next.getDate() + parsed.interval);
|
|
8538
|
-
} else if (parsed.unit === "week") {
|
|
8539
|
-
next.setDate(next.getDate() + parsed.interval * 7);
|
|
8540
|
-
} else if (parsed.unit === "month") {
|
|
8541
|
-
next.setMonth(next.getMonth() + parsed.interval);
|
|
8542
|
-
}
|
|
8543
|
-
return next.toISOString();
|
|
8544
|
-
}
|
|
8545
|
-
if (parsed.type === "specific_days") {
|
|
8546
|
-
const currentDay = base.getDay();
|
|
8547
|
-
const days = parsed.days;
|
|
8548
|
-
let daysToAdd = Infinity;
|
|
8549
|
-
for (const day of days) {
|
|
8550
|
-
let diff = day - currentDay;
|
|
8551
|
-
if (diff <= 0)
|
|
8552
|
-
diff += 7;
|
|
8553
|
-
if (diff < daysToAdd)
|
|
8554
|
-
daysToAdd = diff;
|
|
8555
|
-
}
|
|
8556
|
-
const next = new Date(base);
|
|
8557
|
-
next.setDate(next.getDate() + daysToAdd);
|
|
8558
|
-
return next.toISOString();
|
|
8559
|
-
}
|
|
8560
|
-
throw new Error(`Cannot calculate next occurrence for rule: "${rule}"`);
|
|
8561
|
-
}
|
|
8562
|
-
var DAY_NAMES;
|
|
8563
|
-
var init_recurrence = __esm(() => {
|
|
8564
|
-
DAY_NAMES = {
|
|
8565
|
-
sunday: 0,
|
|
8566
|
-
sun: 0,
|
|
8567
|
-
monday: 1,
|
|
8568
|
-
mon: 1,
|
|
8569
|
-
tuesday: 2,
|
|
8570
|
-
tue: 2,
|
|
8571
|
-
wednesday: 3,
|
|
8572
|
-
wed: 3,
|
|
8573
|
-
thursday: 4,
|
|
8574
|
-
thu: 4,
|
|
8575
|
-
friday: 5,
|
|
8576
|
-
fri: 5,
|
|
8577
|
-
saturday: 6,
|
|
8578
|
-
sat: 6
|
|
8579
|
-
};
|
|
8580
|
-
});
|
|
8581
|
-
|
|
8582
|
-
// src/db/templates.ts
|
|
8583
|
-
var exports_templates = {};
|
|
8584
|
-
__export(exports_templates, {
|
|
8585
|
-
updateTemplate: () => updateTemplate,
|
|
8586
|
-
tasksFromTemplate: () => tasksFromTemplate,
|
|
8587
|
-
taskFromTemplate: () => taskFromTemplate,
|
|
8588
|
-
resolveVariables: () => resolveVariables,
|
|
8589
|
-
previewTemplate: () => previewTemplate,
|
|
8590
|
-
listTemplates: () => listTemplates,
|
|
8591
|
-
listTemplateVersions: () => listTemplateVersions,
|
|
8592
|
-
importTemplate: () => importTemplate,
|
|
8593
|
-
getTemplateWithTasks: () => getTemplateWithTasks,
|
|
8594
|
-
getTemplateVersion: () => getTemplateVersion,
|
|
8595
|
-
getTemplateTasks: () => getTemplateTasks,
|
|
8596
|
-
getTemplate: () => getTemplate,
|
|
8597
|
-
exportTemplate: () => exportTemplate,
|
|
8598
|
-
evaluateCondition: () => evaluateCondition,
|
|
8599
|
-
deleteTemplate: () => deleteTemplate,
|
|
8600
|
-
createTemplate: () => createTemplate,
|
|
8601
|
-
addTemplateTasks: () => addTemplateTasks
|
|
8602
|
-
});
|
|
8603
|
-
function rowToTemplate(row) {
|
|
8604
|
-
return {
|
|
8605
|
-
...row,
|
|
8606
|
-
tags: JSON.parse(row.tags || "[]"),
|
|
8607
|
-
variables: JSON.parse(row.variables || "[]"),
|
|
8608
|
-
metadata: JSON.parse(row.metadata || "{}"),
|
|
8609
|
-
priority: row.priority || "medium",
|
|
8610
|
-
version: row.version ?? 1
|
|
8611
|
-
};
|
|
8612
|
-
}
|
|
8613
|
-
function rowToTemplateTask(row) {
|
|
8614
|
-
return {
|
|
8615
|
-
...row,
|
|
8616
|
-
tags: JSON.parse(row.tags || "[]"),
|
|
8617
|
-
depends_on_positions: JSON.parse(row.depends_on_positions || "[]"),
|
|
8618
|
-
metadata: JSON.parse(row.metadata || "{}"),
|
|
8619
|
-
priority: row.priority || "medium",
|
|
8620
|
-
condition: row.condition ?? null,
|
|
8621
|
-
include_template_id: row.include_template_id ?? null
|
|
8622
|
-
};
|
|
8623
|
-
}
|
|
8624
|
-
function resolveTemplateId(id, d) {
|
|
8625
|
-
return resolvePartialId(d, "task_templates", id);
|
|
8626
|
-
}
|
|
8627
|
-
function createTemplate(input, db) {
|
|
8628
|
-
const d = db || getDatabase();
|
|
8629
|
-
const id = uuid();
|
|
8630
|
-
const machineId = currentStorageMachineId(d);
|
|
8631
|
-
d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, variables, project_id, plan_id, metadata, created_at, machine_id)
|
|
8632
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
8633
|
-
id,
|
|
8634
|
-
input.name,
|
|
8635
|
-
input.title_pattern,
|
|
8636
|
-
input.description || null,
|
|
8637
|
-
input.priority || "medium",
|
|
8638
|
-
JSON.stringify(input.tags || []),
|
|
8639
|
-
JSON.stringify(input.variables || []),
|
|
8640
|
-
input.project_id || null,
|
|
8641
|
-
input.plan_id || null,
|
|
8642
|
-
JSON.stringify(input.metadata || {}),
|
|
8643
|
-
now(),
|
|
8644
|
-
machineId
|
|
8645
|
-
]);
|
|
8646
|
-
if (input.tasks && input.tasks.length > 0) {
|
|
8647
|
-
addTemplateTasks(id, input.tasks, d);
|
|
8648
|
-
}
|
|
8649
|
-
return getTemplate(id, d);
|
|
8650
|
-
}
|
|
8651
|
-
function getTemplate(id, db) {
|
|
8652
|
-
const d = db || getDatabase();
|
|
8653
|
-
const resolved = resolveTemplateId(id, d);
|
|
8654
|
-
if (!resolved)
|
|
8655
|
-
return null;
|
|
8656
|
-
const row = d.query("SELECT * FROM task_templates WHERE id = ?").get(resolved);
|
|
8657
|
-
return row ? rowToTemplate(row) : null;
|
|
8658
|
-
}
|
|
8659
|
-
function listTemplates(db) {
|
|
8660
|
-
const d = db || getDatabase();
|
|
8661
|
-
return d.query("SELECT * FROM task_templates ORDER BY name").all().map(rowToTemplate);
|
|
8662
|
-
}
|
|
8663
|
-
function deleteTemplate(id, db) {
|
|
8664
|
-
const d = db || getDatabase();
|
|
8665
|
-
const resolved = resolveTemplateId(id, d);
|
|
8666
|
-
if (!resolved)
|
|
8667
|
-
return false;
|
|
8668
|
-
const template = getTemplate(resolved, d);
|
|
8669
|
-
if (!template)
|
|
8670
|
-
return false;
|
|
8671
|
-
recordStorageTombstone({
|
|
8672
|
-
object_type: "templates",
|
|
8673
|
-
object_id: resolved,
|
|
8674
|
-
payload: template,
|
|
8675
|
-
version: template.version
|
|
8676
|
-
}, d);
|
|
8677
|
-
return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
|
|
8678
|
-
}
|
|
8679
|
-
function updateTemplate(id, updates, db) {
|
|
8680
|
-
const d = db || getDatabase();
|
|
8681
|
-
const resolved = resolveTemplateId(id, d);
|
|
8682
|
-
if (!resolved)
|
|
8683
|
-
return null;
|
|
8684
|
-
const current = getTemplateWithTasks(resolved, d);
|
|
8685
|
-
if (current) {
|
|
8686
|
-
const snapshot = JSON.stringify({
|
|
8687
|
-
name: current.name,
|
|
8688
|
-
title_pattern: current.title_pattern,
|
|
8689
|
-
description: current.description,
|
|
8690
|
-
priority: current.priority,
|
|
8691
|
-
tags: current.tags,
|
|
8692
|
-
variables: current.variables,
|
|
8693
|
-
project_id: current.project_id,
|
|
8694
|
-
plan_id: current.plan_id,
|
|
8695
|
-
metadata: current.metadata,
|
|
8696
|
-
tasks: current.tasks
|
|
8697
|
-
});
|
|
8698
|
-
d.run(`INSERT INTO template_versions (id, template_id, version, snapshot, created_at) VALUES (?, ?, ?, ?, ?)`, [uuid(), resolved, current.version, snapshot, now()]);
|
|
8699
|
-
}
|
|
8700
|
-
const sets = ["version = version + 1"];
|
|
8701
|
-
const values = [];
|
|
8702
|
-
if (updates.name !== undefined) {
|
|
8703
|
-
sets.push("name = ?");
|
|
8704
|
-
values.push(updates.name);
|
|
8705
|
-
}
|
|
8706
|
-
if (updates.title_pattern !== undefined) {
|
|
8707
|
-
sets.push("title_pattern = ?");
|
|
8708
|
-
values.push(updates.title_pattern);
|
|
8709
|
-
}
|
|
8710
|
-
if (updates.description !== undefined) {
|
|
8711
|
-
sets.push("description = ?");
|
|
8712
|
-
values.push(updates.description);
|
|
8713
|
-
}
|
|
8714
|
-
if (updates.priority !== undefined) {
|
|
8715
|
-
sets.push("priority = ?");
|
|
8716
|
-
values.push(updates.priority);
|
|
8717
|
-
}
|
|
8718
|
-
if (updates.tags !== undefined) {
|
|
8719
|
-
sets.push("tags = ?");
|
|
8720
|
-
values.push(JSON.stringify(updates.tags));
|
|
8721
|
-
}
|
|
8722
|
-
if (updates.variables !== undefined) {
|
|
8723
|
-
sets.push("variables = ?");
|
|
8724
|
-
values.push(JSON.stringify(updates.variables));
|
|
8725
|
-
}
|
|
8726
|
-
if (updates.project_id !== undefined) {
|
|
8727
|
-
sets.push("project_id = ?");
|
|
8728
|
-
values.push(updates.project_id);
|
|
8729
|
-
}
|
|
8730
|
-
if (updates.plan_id !== undefined) {
|
|
8731
|
-
sets.push("plan_id = ?");
|
|
8732
|
-
values.push(updates.plan_id);
|
|
8733
|
-
}
|
|
8734
|
-
if (updates.metadata !== undefined) {
|
|
8735
|
-
sets.push("metadata = ?");
|
|
8736
|
-
values.push(JSON.stringify(updates.metadata));
|
|
8737
|
-
}
|
|
8738
|
-
values.push(resolved);
|
|
8739
|
-
d.run(`UPDATE task_templates SET ${sets.join(", ")} WHERE id = ?`, values);
|
|
8740
|
-
return getTemplate(resolved, d);
|
|
8741
|
-
}
|
|
8742
|
-
function taskFromTemplate(templateId, overrides = {}, db) {
|
|
8743
|
-
const t = getTemplate(templateId, db);
|
|
8744
|
-
if (!t)
|
|
8745
|
-
throw new Error(`Template not found: ${templateId}`);
|
|
8746
|
-
const cleanOverrides = Object.fromEntries(Object.entries(overrides).filter(([, v]) => v !== undefined));
|
|
8747
|
-
return {
|
|
8748
|
-
title: cleanOverrides.title || t.title_pattern,
|
|
8749
|
-
description: cleanOverrides.description ?? t.description ?? undefined,
|
|
8750
|
-
priority: cleanOverrides.priority ?? t.priority,
|
|
8751
|
-
tags: cleanOverrides.tags ?? t.tags,
|
|
8752
|
-
project_id: cleanOverrides.project_id ?? t.project_id ?? undefined,
|
|
8753
|
-
plan_id: cleanOverrides.plan_id ?? t.plan_id ?? undefined,
|
|
8754
|
-
metadata: cleanOverrides.metadata ?? t.metadata,
|
|
8755
|
-
...cleanOverrides
|
|
8756
|
-
};
|
|
8757
|
-
}
|
|
8758
|
-
function addTemplateTasks(templateId, tasks, db) {
|
|
8222
|
+
function addTemplateTasks(templateId, tasks, db) {
|
|
8759
8223
|
const d = db || getDatabase();
|
|
8760
8224
|
const template = getTemplate(templateId, d);
|
|
8761
8225
|
if (!template)
|
|
@@ -9151,527 +8615,1188 @@ function moveTask(taskId, target, db) {
|
|
|
9151
8615
|
sets.push("project_id = ?");
|
|
9152
8616
|
params.push(target.project_id);
|
|
9153
8617
|
}
|
|
9154
|
-
if (target.plan_id !== undefined) {
|
|
9155
|
-
sets.push("plan_id = ?");
|
|
9156
|
-
params.push(target.plan_id);
|
|
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)
|
|
8670
|
+
return null;
|
|
8671
|
+
return new Date(new Date(lockedAt).getTime() + LOCK_EXPIRY_MINUTES * 60 * 1000).toISOString();
|
|
8672
|
+
}
|
|
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}`);
|
|
8679
|
+
}
|
|
8680
|
+
function getBlockingDeps(id, db) {
|
|
8681
|
+
const d = db || getDatabase();
|
|
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;
|
|
8692
|
+
}
|
|
8693
|
+
function startTask(id, agentId, db) {
|
|
8694
|
+
const d = db || getDatabase();
|
|
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
|
|
8712
|
+
});
|
|
8713
|
+
throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
|
|
8714
|
+
}
|
|
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`);
|
|
8728
|
+
}
|
|
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;
|
|
8745
|
+
}
|
|
8746
|
+
if (task.status === "cancelled") {
|
|
8747
|
+
throw new Error(`Task ${id} is cancelled and cannot be completed`);
|
|
8748
|
+
}
|
|
8749
|
+
if (agentId && task.locked_by && task.locked_by !== agentId && !isLockExpired(task.locked_at)) {
|
|
8750
|
+
throw new LockError(id, task.locked_by);
|
|
8751
|
+
}
|
|
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 };
|
|
8760
|
+
}
|
|
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
|
+
}
|
|
8807
|
+
}
|
|
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
|
+
}
|
|
8825
|
+
}
|
|
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 };
|
|
8829
|
+
}
|
|
8830
|
+
if (spawnedFromTemplate) {
|
|
8831
|
+
meta._spawned_task = { id: spawnedFromTemplate.id, short_id: spawnedFromTemplate.short_id, title: spawnedFromTemplate.title };
|
|
8832
|
+
}
|
|
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
|
+
}
|
|
8851
|
+
}
|
|
8852
|
+
return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: finalVersion, updated_at: timestamp, metadata: meta };
|
|
8853
|
+
}
|
|
8854
|
+
function lockTask(id, agentId, db) {
|
|
8855
|
+
const d = db || getDatabase();
|
|
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
|
+
};
|
|
8864
|
+
}
|
|
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) };
|
|
8870
|
+
}
|
|
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
|
+
};
|
|
8897
|
+
}
|
|
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);
|
|
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]);
|
|
8912
|
+
return true;
|
|
8913
|
+
}
|
|
8914
|
+
function getTaskLockStatus(id, db) {
|
|
8915
|
+
const d = db || getDatabase();
|
|
8916
|
+
const task = getTask(id, d);
|
|
8917
|
+
if (!task)
|
|
8918
|
+
throw new TaskNotFoundError(id);
|
|
8919
|
+
const expired = isLockExpired(task.locked_at);
|
|
8920
|
+
return {
|
|
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
|
|
8927
|
+
};
|
|
8928
|
+
}
|
|
8929
|
+
function claimNextTask(agentId, filters, db) {
|
|
8930
|
+
const d = db || getDatabase();
|
|
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;
|
|
8951
|
+
}
|
|
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);
|
|
8960
|
+
}
|
|
8961
|
+
if (filters?.task_list_id) {
|
|
8962
|
+
conditions.push("task_list_id = ?");
|
|
8963
|
+
params.push(filters.task_list_id);
|
|
8964
|
+
}
|
|
8965
|
+
if (filters?.plan_id) {
|
|
8966
|
+
conditions.push("plan_id = ?");
|
|
8967
|
+
params.push(filters.plan_id);
|
|
8968
|
+
}
|
|
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;
|
|
8994
|
+
}
|
|
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);
|
|
9003
|
+
}
|
|
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;
|
|
9013
|
+
}
|
|
9014
|
+
function getTasksChangedSince(since, filters, db) {
|
|
9015
|
+
const d = db || getDatabase();
|
|
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);
|
|
9021
|
+
}
|
|
9022
|
+
if (filters?.task_list_id) {
|
|
9023
|
+
conditions.push("task_list_id = ?");
|
|
9024
|
+
params.push(filters.task_list_id);
|
|
9025
|
+
}
|
|
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
|
|
9044
|
+
}
|
|
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);
|
|
9053
|
+
}
|
|
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);
|
|
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]);
|
|
9101
|
+
}
|
|
9102
|
+
}
|
|
9103
|
+
return { task: failedTask, retryTask };
|
|
9104
|
+
}
|
|
9105
|
+
function getStaleTasks(staleQuery = 30, filters, db) {
|
|
9106
|
+
const d = db || getDatabase();
|
|
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);
|
|
9157
9122
|
}
|
|
9158
|
-
|
|
9159
|
-
d.
|
|
9160
|
-
return
|
|
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);
|
|
9161
9126
|
}
|
|
9162
|
-
function
|
|
9163
|
-
const
|
|
9164
|
-
const
|
|
9165
|
-
|
|
9166
|
-
|
|
9167
|
-
|
|
9168
|
-
|
|
9169
|
-
|
|
9170
|
-
|
|
9171
|
-
|
|
9172
|
-
|
|
9173
|
-
|
|
9174
|
-
|
|
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 };
|
|
9175
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);
|
|
9176
9173
|
}
|
|
9177
|
-
|
|
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);
|
|
9178
9191
|
}
|
|
9179
|
-
var
|
|
9192
|
+
var MAX_SPAWN_DEPTH = 10;
|
|
9193
|
+
var init_task_lifecycle = __esm(() => {
|
|
9180
9194
|
init_types();
|
|
9181
9195
|
init_database();
|
|
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();
|
|
9182
9204
|
init_task_crud();
|
|
9205
|
+
init_task_graph();
|
|
9183
9206
|
});
|
|
9184
9207
|
|
|
9185
|
-
// src/db/task-
|
|
9186
|
-
function
|
|
9187
|
-
|
|
9188
|
-
|
|
9189
|
-
|
|
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
|
+
};
|
|
9190
9218
|
}
|
|
9191
|
-
function
|
|
9192
|
-
if (
|
|
9193
|
-
return;
|
|
9194
|
-
if (task.status === "in_progress")
|
|
9219
|
+
function insertTaskTags(taskId, tags, db) {
|
|
9220
|
+
if (tags.length === 0)
|
|
9195
9221
|
return;
|
|
9196
|
-
|
|
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);
|
|
9226
|
+
}
|
|
9197
9227
|
}
|
|
9198
|
-
function
|
|
9199
|
-
|
|
9200
|
-
|
|
9201
|
-
|
|
9202
|
-
|
|
9203
|
-
|
|
9204
|
-
|
|
9205
|
-
|
|
9206
|
-
if (
|
|
9207
|
-
|
|
9228
|
+
function replaceTaskTags(taskId, tags, db) {
|
|
9229
|
+
db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
|
|
9230
|
+
insertTaskTags(taskId, tags, db);
|
|
9231
|
+
}
|
|
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));
|
|
9208
9241
|
}
|
|
9209
|
-
return blocking;
|
|
9210
9242
|
}
|
|
9211
|
-
function
|
|
9243
|
+
function createTask(input, db) {
|
|
9212
9244
|
const d = db || getDatabase();
|
|
9213
|
-
const databasePath = databasePathFromDatabase(d);
|
|
9214
|
-
const task = getTask(id, d);
|
|
9215
|
-
if (!task)
|
|
9216
|
-
throw new TaskNotFoundError(id);
|
|
9217
|
-
assertStartable(task, agentId);
|
|
9218
|
-
const blocking = getBlockingDeps(id, d);
|
|
9219
|
-
if (blocking.length > 0) {
|
|
9220
|
-
const blockerIds = blocking.map((b) => b.id.slice(0, 8)).join(", ");
|
|
9221
|
-
emitLocalEventHooksQuiet({
|
|
9222
|
-
type: "task.blocked",
|
|
9223
|
-
payload: {
|
|
9224
|
-
id,
|
|
9225
|
-
agent_id: agentId,
|
|
9226
|
-
title: task.title,
|
|
9227
|
-
blockers: blocking.map((b) => ({ id: b.id, short_id: b.short_id, title: b.title, status: b.status }))
|
|
9228
|
-
},
|
|
9229
|
-
databasePath
|
|
9230
|
-
});
|
|
9231
|
-
throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
|
|
9232
|
-
}
|
|
9233
|
-
const cutoff = lockExpiryCutoff();
|
|
9234
9245
|
const timestamp = now();
|
|
9235
|
-
const
|
|
9236
|
-
|
|
9237
|
-
|
|
9238
|
-
|
|
9239
|
-
|
|
9240
|
-
|
|
9241
|
-
|
|
9242
|
-
|
|
9243
|
-
|
|
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;
|
|
9244
9301
|
}
|
|
9245
|
-
throw new Error(`Task ${id} could not be started because it changed during claim`);
|
|
9246
9302
|
}
|
|
9247
|
-
|
|
9248
|
-
|
|
9249
|
-
|
|
9250
|
-
|
|
9251
|
-
|
|
9252
|
-
|
|
9253
|
-
|
|
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;
|
|
9254
9313
|
}
|
|
9255
|
-
function
|
|
9314
|
+
function getTask(id, db) {
|
|
9315
|
+
const d = db || getDatabase();
|
|
9316
|
+
const row = d.query("SELECT * FROM tasks WHERE id = ?").get(id);
|
|
9317
|
+
if (!row)
|
|
9318
|
+
return null;
|
|
9319
|
+
return rowToTask(row);
|
|
9320
|
+
}
|
|
9321
|
+
function getTaskWithRelations(id, db) {
|
|
9256
9322
|
const d = db || getDatabase();
|
|
9257
|
-
const databasePath = databasePathFromDatabase(d);
|
|
9258
9323
|
const task = getTask(id, d);
|
|
9259
9324
|
if (!task)
|
|
9260
|
-
|
|
9261
|
-
|
|
9262
|
-
|
|
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
|
|
9347
|
+
};
|
|
9348
|
+
}
|
|
9349
|
+
function listTasks(filter = {}, db) {
|
|
9350
|
+
const d = db || getDatabase();
|
|
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);
|
|
9263
9358
|
}
|
|
9264
|
-
|
|
9265
|
-
|
|
9266
|
-
|
|
9267
|
-
const completionMeta = {};
|
|
9268
|
-
if (hasEvidence)
|
|
9269
|
-
completionMeta._evidence = evidence;
|
|
9270
|
-
if (options?.confidence !== undefined) {
|
|
9271
|
-
completionMeta._completion = { confidence: options.confidence };
|
|
9359
|
+
if (filter.ids && filter.ids.length > 0) {
|
|
9360
|
+
conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
|
|
9361
|
+
params.push(...filter.ids);
|
|
9272
9362
|
}
|
|
9273
|
-
|
|
9274
|
-
|
|
9275
|
-
|
|
9276
|
-
|
|
9277
|
-
|
|
9278
|
-
|
|
9279
|
-
const metaResult = d.run("UPDATE tasks SET metadata = ?, version = version + 1, updated_at = ? WHERE id = ? AND version = ?", [JSON.stringify(meta2), timestamp, id, task.version]);
|
|
9280
|
-
if (metaResult.changes === 0) {
|
|
9281
|
-
const current = getTask(id, d);
|
|
9282
|
-
throw new VersionConflictError(id, task.version, current?.version ?? -1);
|
|
9283
|
-
}
|
|
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);
|
|
9284
9369
|
}
|
|
9285
|
-
d.run(`UPDATE tasks SET status = 'completed', locked_by = NULL, locked_at = NULL, completed_at = ?, confidence = ?, version = version + 1, updated_at = ?
|
|
9286
|
-
WHERE id = ?`, [timestamp, confidence, timestamp, id]);
|
|
9287
|
-
});
|
|
9288
|
-
tx();
|
|
9289
|
-
logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
|
|
9290
|
-
const completedTaskForEvent = {
|
|
9291
|
-
...task,
|
|
9292
|
-
status: "completed",
|
|
9293
|
-
locked_by: null,
|
|
9294
|
-
locked_at: null,
|
|
9295
|
-
completed_at: timestamp,
|
|
9296
|
-
confidence,
|
|
9297
|
-
version: task.version + 1,
|
|
9298
|
-
updated_at: timestamp,
|
|
9299
|
-
metadata: hasMeta ? { ...task.metadata, ...completionMeta } : task.metadata
|
|
9300
|
-
};
|
|
9301
|
-
const completionPayload = taskEventData(completedTaskForEvent, { agent_id: agentId, completed_at: timestamp });
|
|
9302
|
-
dispatchWebhook2("task.completed", completionPayload, d).catch(() => {});
|
|
9303
|
-
emitLocalEventHooksQuiet({ type: "task.completed", payload: completionPayload, databasePath });
|
|
9304
|
-
emitSharedTaskEventQuiet({ type: "task.completed", task: completedTaskForEvent, data: { agent_id: agentId, completed_at: timestamp }, databasePath });
|
|
9305
|
-
let spawnedTask = null;
|
|
9306
|
-
if (task.recurrence_rule && !options?.skip_recurrence) {
|
|
9307
|
-
spawnedTask = spawnNextRecurrence(task, d, timestamp);
|
|
9308
9370
|
}
|
|
9309
|
-
|
|
9310
|
-
|
|
9311
|
-
|
|
9312
|
-
|
|
9313
|
-
console.warn(`[tasks] Task ${id} exceeded max spawn depth (${MAX_SPAWN_DEPTH}), skipping template spawn`);
|
|
9371
|
+
if (filter.status) {
|
|
9372
|
+
if (Array.isArray(filter.status)) {
|
|
9373
|
+
conditions.push(`status IN (${filter.status.map(() => "?").join(",")})`);
|
|
9374
|
+
params.push(...filter.status);
|
|
9314
9375
|
} else {
|
|
9315
|
-
|
|
9316
|
-
|
|
9317
|
-
project_id: task.project_id ?? undefined,
|
|
9318
|
-
plan_id: task.plan_id ?? undefined,
|
|
9319
|
-
task_list_id: task.task_list_id ?? undefined,
|
|
9320
|
-
assigned_to: task.assigned_to ?? undefined
|
|
9321
|
-
}, d);
|
|
9322
|
-
input.metadata = { ...input.metadata || {}, _spawn_depth: spawnDepth + 1 };
|
|
9323
|
-
spawnedFromTemplate = createTask(input, d);
|
|
9324
|
-
} catch {}
|
|
9376
|
+
conditions.push("status = ?");
|
|
9377
|
+
params.push(filter.status);
|
|
9325
9378
|
}
|
|
9326
9379
|
}
|
|
9327
|
-
|
|
9328
|
-
|
|
9329
|
-
|
|
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
|
+
}
|
|
9330
9388
|
}
|
|
9331
|
-
if (
|
|
9332
|
-
|
|
9389
|
+
if (filter.assigned_to) {
|
|
9390
|
+
conditions.push("assigned_to = ?");
|
|
9391
|
+
params.push(filter.assigned_to);
|
|
9333
9392
|
}
|
|
9334
|
-
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
AND NOT EXISTS (
|
|
9338
|
-
SELECT 1 FROM task_dependencies td2
|
|
9339
|
-
JOIN tasks dep2 ON dep2.id = td2.depends_on
|
|
9340
|
-
WHERE td2.task_id = t.id AND dep2.status NOT IN ('completed', 'cancelled') AND dep2.id != ?
|
|
9341
|
-
)`).all(id, id);
|
|
9342
|
-
if (unblockedDeps.length > 0) {
|
|
9343
|
-
meta._unblocked = unblockedDeps.map((d2) => ({ id: d2.id, short_id: d2.short_id, title: d2.title }));
|
|
9344
|
-
for (const dep of unblockedDeps) {
|
|
9345
|
-
const depTask = getTask(dep.id, d);
|
|
9346
|
-
const payload = depTask ? taskEventData(depTask, { unblocked_by: id }) : { id: dep.id, unblocked_by: id, title: dep.title };
|
|
9347
|
-
dispatchWebhook2("task.unblocked", payload, d).catch(() => {});
|
|
9348
|
-
emitLocalEventHooksQuiet({ type: "task.unblocked", payload, databasePath });
|
|
9349
|
-
if (depTask)
|
|
9350
|
-
emitSharedTaskEventQuiet({ type: "task.unblocked", task: depTask, data: { unblocked_by: id }, databasePath });
|
|
9351
|
-
}
|
|
9393
|
+
if (filter.agent_id) {
|
|
9394
|
+
conditions.push("agent_id = ?");
|
|
9395
|
+
params.push(filter.agent_id);
|
|
9352
9396
|
}
|
|
9353
|
-
|
|
9354
|
-
|
|
9355
|
-
|
|
9356
|
-
const d = db || getDatabase();
|
|
9357
|
-
const task = getTask(id, d);
|
|
9358
|
-
if (!task)
|
|
9359
|
-
throw new TaskNotFoundError(id);
|
|
9360
|
-
if (task.status === "completed" || task.status === "cancelled") {
|
|
9361
|
-
return {
|
|
9362
|
-
success: false,
|
|
9363
|
-
error: `Task is ${task.status} and cannot be locked`
|
|
9364
|
-
};
|
|
9397
|
+
if (filter.session_id) {
|
|
9398
|
+
conditions.push("session_id = ?");
|
|
9399
|
+
params.push(filter.session_id);
|
|
9365
9400
|
}
|
|
9366
|
-
if (
|
|
9367
|
-
const
|
|
9368
|
-
|
|
9369
|
-
|
|
9370
|
-
return { success: true, locked_by: agentId, locked_at: timestamp2, expires_at: lockExpiresAt(timestamp2) };
|
|
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);
|
|
9371
9405
|
}
|
|
9372
|
-
|
|
9373
|
-
|
|
9374
|
-
|
|
9375
|
-
|
|
9376
|
-
if (
|
|
9377
|
-
|
|
9378
|
-
|
|
9379
|
-
|
|
9380
|
-
|
|
9381
|
-
|
|
9382
|
-
|
|
9383
|
-
|
|
9384
|
-
|
|
9385
|
-
|
|
9386
|
-
if (
|
|
9387
|
-
|
|
9388
|
-
|
|
9389
|
-
|
|
9390
|
-
|
|
9391
|
-
|
|
9392
|
-
};
|
|
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);
|
|
9393
9426
|
}
|
|
9394
|
-
return {
|
|
9395
|
-
success: false,
|
|
9396
|
-
error: `Task ${id} could not be locked because it changed during lock acquisition`
|
|
9397
|
-
};
|
|
9398
9427
|
}
|
|
9399
|
-
|
|
9400
|
-
|
|
9401
|
-
|
|
9402
|
-
|
|
9403
|
-
|
|
9404
|
-
|
|
9405
|
-
|
|
9406
|
-
|
|
9407
|
-
|
|
9408
|
-
|
|
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
|
+
}
|
|
9409
9449
|
}
|
|
9410
|
-
const
|
|
9411
|
-
|
|
9412
|
-
WHERE id = ?`, [timestamp, id]);
|
|
9413
|
-
return true;
|
|
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);
|
|
9414
9452
|
}
|
|
9415
|
-
function
|
|
9416
|
-
const
|
|
9417
|
-
|
|
9418
|
-
|
|
9419
|
-
|
|
9420
|
-
const expired = isLockExpired(task.locked_at);
|
|
9453
|
+
function getTaskByFingerprint(fingerprint, db) {
|
|
9454
|
+
const tasks = listTasks({ metadata: { fingerprint }, limit: 1, include_archived: true }, db);
|
|
9455
|
+
return tasks[0] ?? null;
|
|
9456
|
+
}
|
|
9457
|
+
function mergeTaskMetadata(current, next, fingerprint) {
|
|
9421
9458
|
return {
|
|
9422
|
-
|
|
9423
|
-
|
|
9424
|
-
|
|
9425
|
-
locked_at: task.locked_at,
|
|
9426
|
-
expires_at: lockExpiresAt(task.locked_at),
|
|
9427
|
-
expired
|
|
9459
|
+
...current,
|
|
9460
|
+
...next ?? {},
|
|
9461
|
+
fingerprint
|
|
9428
9462
|
};
|
|
9429
9463
|
}
|
|
9430
|
-
function
|
|
9464
|
+
function upsertTaskByFingerprint(input, db) {
|
|
9431
9465
|
const d = db || getDatabase();
|
|
9466
|
+
const fingerprint = input.fingerprint.trim();
|
|
9467
|
+
if (!fingerprint)
|
|
9468
|
+
throw new Error("fingerprint is required");
|
|
9432
9469
|
const tx = d.transaction(() => {
|
|
9433
|
-
const
|
|
9434
|
-
|
|
9435
|
-
|
|
9436
|
-
|
|
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 };
|
|
9437
9501
|
});
|
|
9438
9502
|
return tx();
|
|
9439
9503
|
}
|
|
9440
|
-
function
|
|
9504
|
+
function countTasks(filter = {}, db) {
|
|
9441
9505
|
const d = db || getDatabase();
|
|
9442
|
-
|
|
9443
|
-
const
|
|
9444
|
-
|
|
9445
|
-
if (filters?.project_id) {
|
|
9506
|
+
const conditions = [];
|
|
9507
|
+
const params = [];
|
|
9508
|
+
if (filter.project_id) {
|
|
9446
9509
|
conditions.push("project_id = ?");
|
|
9447
|
-
params.push(
|
|
9510
|
+
params.push(filter.project_id);
|
|
9448
9511
|
}
|
|
9449
|
-
if (
|
|
9450
|
-
conditions.push(
|
|
9451
|
-
params.push(
|
|
9512
|
+
if (filter.ids && filter.ids.length > 0) {
|
|
9513
|
+
conditions.push(`id IN (${filter.ids.map(() => "?").join(",")})`);
|
|
9514
|
+
params.push(...filter.ids);
|
|
9452
9515
|
}
|
|
9453
|
-
if (
|
|
9454
|
-
|
|
9455
|
-
|
|
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
|
+
}
|
|
9456
9523
|
}
|
|
9457
|
-
if (
|
|
9458
|
-
|
|
9459
|
-
|
|
9460
|
-
|
|
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);
|
|
9531
|
+
}
|
|
9461
9532
|
}
|
|
9462
|
-
|
|
9463
|
-
|
|
9464
|
-
|
|
9465
|
-
|
|
9466
|
-
|
|
9467
|
-
|
|
9533
|
+
if (filter.priority) {
|
|
9534
|
+
if (Array.isArray(filter.priority)) {
|
|
9535
|
+
conditions.push(`priority IN (${filter.priority.map(() => "?").join(",")})`);
|
|
9536
|
+
params.push(...filter.priority);
|
|
9537
|
+
} else {
|
|
9538
|
+
conditions.push("priority = ?");
|
|
9539
|
+
params.push(filter.priority);
|
|
9540
|
+
}
|
|
9468
9541
|
}
|
|
9469
|
-
|
|
9470
|
-
|
|
9471
|
-
|
|
9472
|
-
params.push(agentId);
|
|
9542
|
+
if (filter.assigned_to) {
|
|
9543
|
+
conditions.push("assigned_to = ?");
|
|
9544
|
+
params.push(filter.assigned_to);
|
|
9473
9545
|
}
|
|
9474
|
-
if (
|
|
9475
|
-
|
|
9476
|
-
|
|
9477
|
-
params.push(...recentProjectIds);
|
|
9546
|
+
if (filter.agent_id) {
|
|
9547
|
+
conditions.push("agent_id = ?");
|
|
9548
|
+
params.push(filter.agent_id);
|
|
9478
9549
|
}
|
|
9479
|
-
|
|
9480
|
-
|
|
9481
|
-
|
|
9482
|
-
}
|
|
9483
|
-
function getActiveWork(filters, db) {
|
|
9484
|
-
const d = db || getDatabase();
|
|
9485
|
-
clearExpiredLocks(d);
|
|
9486
|
-
const conditions = ["status = 'in_progress'"];
|
|
9487
|
-
const params = [];
|
|
9488
|
-
if (filters?.project_id) {
|
|
9489
|
-
conditions.push("project_id = ?");
|
|
9490
|
-
params.push(filters.project_id);
|
|
9550
|
+
if (filter.session_id) {
|
|
9551
|
+
conditions.push("session_id = ?");
|
|
9552
|
+
params.push(filter.session_id);
|
|
9491
9553
|
}
|
|
9492
|
-
if (
|
|
9493
|
-
|
|
9494
|
-
|
|
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);
|
|
9495
9558
|
}
|
|
9496
|
-
|
|
9497
|
-
|
|
9498
|
-
|
|
9499
|
-
updated_at DESC`).all(...params);
|
|
9500
|
-
return rows;
|
|
9501
|
-
}
|
|
9502
|
-
function getTasksChangedSince(since, filters, db) {
|
|
9503
|
-
const d = db || getDatabase();
|
|
9504
|
-
const conditions = ["updated_at > ?"];
|
|
9505
|
-
const params = [since];
|
|
9506
|
-
if (filters?.project_id) {
|
|
9507
|
-
conditions.push("project_id = ?");
|
|
9508
|
-
params.push(filters.project_id);
|
|
9559
|
+
if (filter.plan_id) {
|
|
9560
|
+
conditions.push("plan_id = ?");
|
|
9561
|
+
params.push(filter.plan_id);
|
|
9509
9562
|
}
|
|
9510
|
-
if (
|
|
9563
|
+
if (filter.task_list_id) {
|
|
9511
9564
|
conditions.push("task_list_id = ?");
|
|
9512
|
-
params.push(
|
|
9565
|
+
params.push(filter.task_list_id);
|
|
9513
9566
|
}
|
|
9514
|
-
|
|
9515
|
-
|
|
9516
|
-
|
|
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);
|
|
9579
|
+
}
|
|
9580
|
+
}
|
|
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;
|
|
9517
9588
|
}
|
|
9518
|
-
function
|
|
9589
|
+
function updateTask(id, input, db) {
|
|
9519
9590
|
const d = db || getDatabase();
|
|
9520
|
-
const databasePath = databasePathFromDatabase(d);
|
|
9521
9591
|
const task = getTask(id, d);
|
|
9522
9592
|
if (!task)
|
|
9523
9593
|
throw new TaskNotFoundError(id);
|
|
9524
|
-
|
|
9525
|
-
|
|
9526
|
-
|
|
9527
|
-
reason: reason || "Unknown failure",
|
|
9528
|
-
error_code: options?.error_code || null,
|
|
9529
|
-
failed_by: agentId || null,
|
|
9530
|
-
failed_at: now(),
|
|
9531
|
-
retry_requested: options?.retry || false
|
|
9532
|
-
}
|
|
9533
|
-
};
|
|
9594
|
+
if (task.version !== input.version) {
|
|
9595
|
+
throw new VersionConflictError(id, input.version, task.version);
|
|
9596
|
+
}
|
|
9534
9597
|
const timestamp = now();
|
|
9535
|
-
|
|
9536
|
-
|
|
9537
|
-
const
|
|
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);
|
|
9612
|
+
}
|
|
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");
|
|
9622
|
+
}
|
|
9623
|
+
}
|
|
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);
|
|
9667
|
+
}
|
|
9668
|
+
if (input.actual_minutes !== undefined) {
|
|
9669
|
+
sets.push("actual_minutes = ?");
|
|
9670
|
+
params.push(input.actual_minutes);
|
|
9671
|
+
}
|
|
9672
|
+
if (input.completed_at !== undefined && input.status !== "completed") {
|
|
9673
|
+
sets.push("completed_at = ?");
|
|
9674
|
+
params.push(input.completed_at);
|
|
9675
|
+
}
|
|
9676
|
+
if (input.confidence !== undefined) {
|
|
9677
|
+
sets.push("confidence = ?");
|
|
9678
|
+
params.push(input.confidence);
|
|
9679
|
+
}
|
|
9680
|
+
if (input.retry_count !== undefined) {
|
|
9681
|
+
sets.push("retry_count = ?");
|
|
9682
|
+
params.push(input.retry_count);
|
|
9683
|
+
}
|
|
9684
|
+
if (input.max_retries !== undefined) {
|
|
9685
|
+
sets.push("max_retries = ?");
|
|
9686
|
+
params.push(input.max_retries);
|
|
9687
|
+
}
|
|
9688
|
+
if (input.retry_after !== undefined) {
|
|
9689
|
+
sets.push("retry_after = ?");
|
|
9690
|
+
params.push(input.retry_after);
|
|
9691
|
+
}
|
|
9692
|
+
if (input.requires_approval !== undefined) {
|
|
9693
|
+
sets.push("requires_approval = ?");
|
|
9694
|
+
params.push(input.requires_approval ? 1 : 0);
|
|
9695
|
+
}
|
|
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());
|
|
9701
|
+
}
|
|
9702
|
+
if (input.recurrence_rule !== undefined) {
|
|
9703
|
+
sets.push("recurrence_rule = ?");
|
|
9704
|
+
params.push(input.recurrence_rule);
|
|
9705
|
+
}
|
|
9706
|
+
if (input.task_type !== undefined) {
|
|
9707
|
+
sets.push("task_type = ?");
|
|
9708
|
+
params.push(input.task_type ?? null);
|
|
9709
|
+
}
|
|
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);
|
|
9715
|
+
}
|
|
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)}`);
|
|
9726
|
+
}
|
|
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 = {
|
|
9538
9744
|
...task,
|
|
9539
|
-
|
|
9540
|
-
|
|
9541
|
-
|
|
9542
|
-
metadata: meta,
|
|
9745
|
+
...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
|
|
9746
|
+
tags: input.tags ?? task.tags,
|
|
9747
|
+
metadata: input.metadata ?? task.metadata,
|
|
9543
9748
|
version: task.version + 1,
|
|
9544
|
-
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
|
|
9545
9762
|
};
|
|
9546
|
-
|
|
9547
|
-
|
|
9548
|
-
|
|
9549
|
-
|
|
9550
|
-
|
|
9551
|
-
|
|
9552
|
-
if (options?.retry) {
|
|
9553
|
-
const retryCount = (task.retry_count || 0) + 1;
|
|
9554
|
-
const maxRetries = task.max_retries || 3;
|
|
9555
|
-
if (retryCount > maxRetries) {
|
|
9556
|
-
d.run("UPDATE tasks SET metadata = ? WHERE id = ?", [
|
|
9557
|
-
JSON.stringify({ ...meta, _retry_exhausted: { retry_count: retryCount - 1, max_retries: maxRetries } }),
|
|
9558
|
-
id
|
|
9559
|
-
]);
|
|
9560
|
-
} else {
|
|
9561
|
-
const backoffMinutes = Math.pow(5, retryCount - 1);
|
|
9562
|
-
const retryAfter = options.retry_after || new Date(Date.now() + backoffMinutes * 60 * 1000).toISOString();
|
|
9563
|
-
let title = task.title;
|
|
9564
|
-
if (task.short_id && title.startsWith(task.short_id + ": ")) {
|
|
9565
|
-
title = title.slice(task.short_id.length + 2);
|
|
9566
|
-
}
|
|
9567
|
-
retryTask = createTask({
|
|
9568
|
-
title,
|
|
9569
|
-
description: task.description ?? undefined,
|
|
9570
|
-
priority: task.priority,
|
|
9571
|
-
project_id: task.project_id ?? undefined,
|
|
9572
|
-
task_list_id: task.task_list_id ?? undefined,
|
|
9573
|
-
plan_id: task.plan_id ?? undefined,
|
|
9574
|
-
assigned_to: task.assigned_to ?? undefined,
|
|
9575
|
-
tags: task.tags,
|
|
9576
|
-
metadata: { ...task.metadata, _retry: { original_id: task.id, retry_count: retryCount, max_retries: maxRetries, retry_after: retryAfter, failure_reason: reason } },
|
|
9577
|
-
estimated_minutes: task.estimated_minutes ?? undefined,
|
|
9578
|
-
recurrence_rule: task.recurrence_rule ?? undefined,
|
|
9579
|
-
due_at: retryAfter
|
|
9580
|
-
}, d);
|
|
9581
|
-
d.run("UPDATE tasks SET retry_count = ?, max_retries = ?, retry_after = ? WHERE id = ?", [retryCount, maxRetries, retryAfter, retryTask.id]);
|
|
9582
|
-
}
|
|
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 });
|
|
9583
9769
|
}
|
|
9584
|
-
|
|
9585
|
-
}
|
|
9586
|
-
|
|
9587
|
-
|
|
9588
|
-
|
|
9589
|
-
const effectiveFilters = typeof staleQuery === "number" ? filters : { project_id: staleQuery.project_id, task_list_id: staleQuery.task_list_id };
|
|
9590
|
-
const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
|
|
9591
|
-
const conditions = [
|
|
9592
|
-
"status = 'in_progress'",
|
|
9593
|
-
"(updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))"
|
|
9594
|
-
];
|
|
9595
|
-
const params = [cutoff, cutoff];
|
|
9596
|
-
if (effectiveFilters?.project_id) {
|
|
9597
|
-
conditions.push("project_id = ?");
|
|
9598
|
-
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 });
|
|
9599
9775
|
}
|
|
9600
|
-
if (
|
|
9601
|
-
|
|
9602
|
-
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 });
|
|
9603
9778
|
}
|
|
9604
|
-
const
|
|
9605
|
-
|
|
9606
|
-
|
|
9607
|
-
}
|
|
9608
|
-
|
|
9609
|
-
const d = db || getDatabase();
|
|
9610
|
-
const databasePath = databasePathFromDatabase(d);
|
|
9611
|
-
const staleMinutes = opts?.stale_minutes ?? 30;
|
|
9612
|
-
const staleTasks = getStaleTasks(staleMinutes, { project_id: opts?.project_id, task_list_id: opts?.task_list_id }, d);
|
|
9613
|
-
if (staleTasks.length === 0)
|
|
9614
|
-
return null;
|
|
9615
|
-
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
9616
|
-
staleTasks.sort((a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9));
|
|
9617
|
-
const target = staleTasks[0];
|
|
9618
|
-
const timestamp = now();
|
|
9619
|
-
const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
|
|
9620
|
-
const result = d.run(`UPDATE tasks SET assigned_to = ?, locked_by = ?, locked_at = ?, updated_at = ?, version = version + 1
|
|
9621
|
-
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]);
|
|
9622
|
-
if (result.changes === 0)
|
|
9623
|
-
return null;
|
|
9624
|
-
logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
|
|
9625
|
-
logTaskChange(target.id, "steal", "locked_by", target.locked_by, agentId, agentId, d);
|
|
9626
|
-
const stolenTask = { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
|
|
9627
|
-
const payload = taskEventData(stolenTask, { agent_id: agentId, stolen_from: target.assigned_to });
|
|
9628
|
-
dispatchWebhook2("task.assigned", payload, d).catch(() => {});
|
|
9629
|
-
emitLocalEventHooksQuiet({ type: "task.assigned", payload, databasePath });
|
|
9630
|
-
emitSharedTaskEventQuiet({ type: "task.assigned", task: stolenTask, data: { agent_id: agentId, stolen_from: target.assigned_to }, databasePath });
|
|
9631
|
-
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;
|
|
9632
9784
|
}
|
|
9633
|
-
function
|
|
9785
|
+
function deleteTask(id, db) {
|
|
9634
9786
|
const d = db || getDatabase();
|
|
9635
|
-
const
|
|
9636
|
-
|
|
9637
|
-
|
|
9638
|
-
|
|
9639
|
-
|
|
9640
|
-
|
|
9641
|
-
|
|
9642
|
-
|
|
9643
|
-
|
|
9644
|
-
|
|
9645
|
-
|
|
9646
|
-
return tx();
|
|
9647
|
-
}
|
|
9648
|
-
function spawnNextRecurrence(completedTask, db, completedAt) {
|
|
9649
|
-
const recurrenceBase = completedTask.due_at ? new Date(completedTask.due_at) : new Date(completedAt);
|
|
9650
|
-
const dueAt = nextOccurrence(completedTask.recurrence_rule, recurrenceBase);
|
|
9651
|
-
let title = completedTask.title;
|
|
9652
|
-
if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
|
|
9653
|
-
title = title.slice(completedTask.short_id.length + 2);
|
|
9654
|
-
}
|
|
9655
|
-
const recurrenceParentId = completedTask.recurrence_parent_id || completedTask.id;
|
|
9656
|
-
return createTask({
|
|
9657
|
-
title,
|
|
9658
|
-
description: completedTask.description ?? undefined,
|
|
9659
|
-
priority: completedTask.priority,
|
|
9660
|
-
project_id: completedTask.project_id ?? undefined,
|
|
9661
|
-
task_list_id: completedTask.task_list_id ?? undefined,
|
|
9662
|
-
plan_id: completedTask.plan_id ?? undefined,
|
|
9663
|
-
assigned_to: completedTask.assigned_to ?? undefined,
|
|
9664
|
-
tags: completedTask.tags,
|
|
9665
|
-
metadata: completedTask.metadata,
|
|
9666
|
-
estimated_minutes: completedTask.estimated_minutes ?? undefined,
|
|
9667
|
-
sla_minutes: completedTask.sla_minutes ?? undefined,
|
|
9668
|
-
recurrence_rule: completedTask.recurrence_rule,
|
|
9669
|
-
recurrence_parent_id: recurrenceParentId,
|
|
9670
|
-
due_at: dueAt
|
|
9671
|
-
}, 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;
|
|
9672
9798
|
}
|
|
9673
|
-
var
|
|
9674
|
-
var init_task_lifecycle = __esm(() => {
|
|
9799
|
+
var init_task_crud = __esm(() => {
|
|
9675
9800
|
init_types();
|
|
9676
9801
|
init_database();
|
|
9677
9802
|
init_completion_guard();
|
|
@@ -9679,11 +9804,9 @@ var init_task_lifecycle = __esm(() => {
|
|
|
9679
9804
|
init_event_hooks();
|
|
9680
9805
|
init_shared_events();
|
|
9681
9806
|
init_audit();
|
|
9682
|
-
init_recurrence();
|
|
9683
9807
|
init_webhooks();
|
|
9684
|
-
|
|
9685
|
-
|
|
9686
|
-
init_task_graph();
|
|
9808
|
+
init_checklists();
|
|
9809
|
+
init_storage_tombstones();
|
|
9687
9810
|
});
|
|
9688
9811
|
|
|
9689
9812
|
// src/db/task-status.ts
|
|
@@ -9773,6 +9896,15 @@ function setTaskStatus(id, status, _agentId, db) {
|
|
|
9773
9896
|
throw new TaskNotFoundError(id);
|
|
9774
9897
|
if (task.status === status)
|
|
9775
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
|
+
}
|
|
9776
9908
|
try {
|
|
9777
9909
|
return updateTask(id, { status, version: task.version }, d);
|
|
9778
9910
|
} catch (e) {
|
|
@@ -12742,11 +12874,6 @@ __export(exports_task_routing, {
|
|
|
12742
12874
|
setTaskWorkflowPointers: () => setTaskWorkflowPointers,
|
|
12743
12875
|
getTaskRouteState: () => getTaskRouteState
|
|
12744
12876
|
});
|
|
12745
|
-
function classifyProjectKind2(path) {
|
|
12746
|
-
if (!path)
|
|
12747
|
-
return null;
|
|
12748
|
-
return path.includes("/hasna/opensource/") ? "open-source" : "unknown";
|
|
12749
|
-
}
|
|
12750
12877
|
function machineLocalPath(project, db) {
|
|
12751
12878
|
const machineId = process.env["TODOS_MACHINE_ID"];
|
|
12752
12879
|
if (!machineId)
|
|
@@ -12794,6 +12921,7 @@ function getTaskRouteState(taskOrId, db) {
|
|
|
12794
12921
|
const automation = routingAutomationMetadata(task, taskList) ?? {};
|
|
12795
12922
|
const routeEnabled = routeEnabledForTask(task, taskList) === true;
|
|
12796
12923
|
const tagOptIn = task.tags.includes("auto:route") || task.tags.includes("route:enabled");
|
|
12924
|
+
const projectKind = projectKindFromMetadata(task.metadata, taskList?.metadata);
|
|
12797
12925
|
const locked = Boolean(task.locked_by && !isLockExpired(task.locked_at));
|
|
12798
12926
|
const blockers = getBlockingDeps(task.id, d);
|
|
12799
12927
|
const blocked = blockers.length > 0;
|
|
@@ -12856,7 +12984,7 @@ function getTaskRouteState(taskOrId, db) {
|
|
|
12856
12984
|
project_id: project?.id ?? task.project_id,
|
|
12857
12985
|
project_path: projectPath,
|
|
12858
12986
|
working_dir: task.working_dir ?? projectPath,
|
|
12859
|
-
project_kind:
|
|
12987
|
+
project_kind: projectKind,
|
|
12860
12988
|
task_list_id: taskList?.id ?? task.task_list_id,
|
|
12861
12989
|
task_list_slug: taskList?.slug ?? null,
|
|
12862
12990
|
task_list_name: taskList?.name ?? null,
|
|
@@ -12925,20 +13053,56 @@ import chalk2 from "chalk";
|
|
|
12925
13053
|
import { basename as basename3, resolve as resolve9 } from "path";
|
|
12926
13054
|
function resolveProjectIdOrSlug(input) {
|
|
12927
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;
|
|
12928
13064
|
const byId = getProject(input, db);
|
|
12929
13065
|
if (byId)
|
|
12930
13066
|
return byId.id;
|
|
12931
|
-
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}%`);
|
|
12932
13081
|
if (row)
|
|
12933
13082
|
return row.id;
|
|
12934
|
-
if (isPathLike(input)) {
|
|
12935
|
-
const projectPath = resolve9(input);
|
|
12936
|
-
const byPath = getProjectByPath(projectPath, db);
|
|
12937
|
-
return (byPath ?? ensureProject(basename3(projectPath), projectPath, db)).id;
|
|
12938
|
-
}
|
|
12939
13083
|
console.error(chalk2.red(`Project not found: ${input}`));
|
|
12940
13084
|
process.exit(1);
|
|
12941
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
|
+
}
|
|
12942
13106
|
function isPathLike(input) {
|
|
12943
13107
|
return input.startsWith(".") || input.includes("/") || input.includes("\\");
|
|
12944
13108
|
}
|
|
@@ -13037,27 +13201,32 @@ function registerTaskCommands(program2) {
|
|
|
13037
13201
|
}
|
|
13038
13202
|
return id;
|
|
13039
13203
|
})() : undefined;
|
|
13040
|
-
|
|
13041
|
-
|
|
13042
|
-
|
|
13043
|
-
|
|
13044
|
-
|
|
13045
|
-
|
|
13046
|
-
|
|
13047
|
-
|
|
13048
|
-
|
|
13049
|
-
|
|
13050
|
-
|
|
13051
|
-
|
|
13052
|
-
|
|
13053
|
-
|
|
13054
|
-
|
|
13055
|
-
|
|
13056
|
-
|
|
13057
|
-
|
|
13058
|
-
|
|
13059
|
-
|
|
13060
|
-
|
|
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
|
+
}
|
|
13061
13230
|
if (globalOpts.json) {
|
|
13062
13231
|
output(task2, true);
|
|
13063
13232
|
} else {
|
|
@@ -13088,7 +13257,7 @@ function registerTaskCommands(program2) {
|
|
|
13088
13257
|
title: opts.title,
|
|
13089
13258
|
description: opts.description,
|
|
13090
13259
|
priority: parsePriority(opts.priority),
|
|
13091
|
-
status:
|
|
13260
|
+
status: parseStatus(opts.status),
|
|
13092
13261
|
task_list_id: taskListId,
|
|
13093
13262
|
tags: parseTags(opts.tags),
|
|
13094
13263
|
metadata: buildExpectationMetadata(opts),
|
|
@@ -13565,7 +13734,7 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
|
|
|
13565
13734
|
console.log(` ${chalk2.dim(h.created_at)} ${chalk2.bold(h.action)}${field}${change}${agent}`);
|
|
13566
13735
|
}
|
|
13567
13736
|
});
|
|
13568
|
-
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) => {
|
|
13569
13738
|
const globalOpts = program2.opts();
|
|
13570
13739
|
opts.tags = opts.tags || opts.tag;
|
|
13571
13740
|
opts.list = opts.list || opts.taskList;
|
|
@@ -13579,6 +13748,10 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
|
|
|
13579
13748
|
console.error(chalk2.red("Use either --plan or --clear-plan, not both."));
|
|
13580
13749
|
process.exit(1);
|
|
13581
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
|
+
}
|
|
13582
13755
|
const taskListId = opts.list ? (() => {
|
|
13583
13756
|
const db = getDatabase();
|
|
13584
13757
|
const resolved = resolvePartialId(db, "task_lists", opts.list);
|
|
@@ -13595,17 +13768,17 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
|
|
|
13595
13768
|
version: current.version,
|
|
13596
13769
|
title: opts.title,
|
|
13597
13770
|
description: opts.description,
|
|
13598
|
-
status:
|
|
13599
|
-
priority: opts.priority,
|
|
13771
|
+
status: parseStatus(opts.status),
|
|
13772
|
+
priority: parsePriority(opts.priority),
|
|
13600
13773
|
assigned_to: opts.assign,
|
|
13601
13774
|
tags: opts.tags ? opts.tags.split(",").map((t) => t.trim()) : undefined,
|
|
13602
13775
|
plan_id: planId,
|
|
13603
13776
|
task_list_id: taskListId,
|
|
13604
|
-
estimated_minutes: opts.estimated !== undefined ?
|
|
13605
|
-
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,
|
|
13606
13779
|
due_at: opts.due !== undefined ? opts.due === "" ? null : opts.due.length === 10 ? opts.due + "T00:00:00.000Z" : opts.due : undefined,
|
|
13607
13780
|
recurrence_rule: opts.recurrence !== undefined ? opts.recurrence === "" ? null : opts.recurrence : undefined,
|
|
13608
|
-
requires_approval: opts.approval !== undefined ? true : undefined
|
|
13781
|
+
requires_approval: opts.clearApproval ? false : opts.approval !== undefined ? true : undefined
|
|
13609
13782
|
});
|
|
13610
13783
|
} catch (e) {
|
|
13611
13784
|
handleError(e);
|
|
@@ -13622,7 +13795,14 @@ ${chalk2.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
|
|
|
13622
13795
|
const resolvedId = resolveTaskId(id);
|
|
13623
13796
|
const attachmentIds = opts.attachIds ? opts.attachIds.split(",").map((s) => s.trim()) : undefined;
|
|
13624
13797
|
const filesChanged = opts.filesChanged ? opts.filesChanged.split(",").map((s) => s.trim()) : undefined;
|
|
13625
|
-
|
|
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
|
+
}
|
|
13626
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;
|
|
13627
13807
|
let task2;
|
|
13628
13808
|
try {
|
|
@@ -23254,12 +23434,21 @@ function registerProjectCommands(program2) {
|
|
|
23254
23434
|
handleError(e);
|
|
23255
23435
|
}
|
|
23256
23436
|
});
|
|
23257
|
-
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) => {
|
|
23258
23438
|
const globalOpts = program2.opts();
|
|
23259
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
|
+
}
|
|
23260
23449
|
const comment = addComment({
|
|
23261
23450
|
task_id: resolvedId,
|
|
23262
|
-
content
|
|
23451
|
+
content,
|
|
23263
23452
|
agent_id: globalOpts.agent,
|
|
23264
23453
|
session_id: globalOpts.session
|
|
23265
23454
|
});
|
|
@@ -25391,10 +25580,8 @@ var init_token_utils = __esm(() => {
|
|
|
25391
25580
|
"cancel_task",
|
|
25392
25581
|
"check_task_done_contract",
|
|
25393
25582
|
"claim_task",
|
|
25394
|
-
"clone_task",
|
|
25395
25583
|
"delete_task",
|
|
25396
25584
|
"extend_task",
|
|
25397
|
-
"get_active_work",
|
|
25398
25585
|
"get_archived_tasks",
|
|
25399
25586
|
"get_blocked_tasks",
|
|
25400
25587
|
"get_blocking_tasks",
|
|
@@ -25430,7 +25617,8 @@ var init_token_utils = __esm(() => {
|
|
|
25430
25617
|
"task_context",
|
|
25431
25618
|
"unlock_task",
|
|
25432
25619
|
"unarchive_task",
|
|
25433
|
-
"update_task"
|
|
25620
|
+
"update_task",
|
|
25621
|
+
"upsert_task"
|
|
25434
25622
|
],
|
|
25435
25623
|
projects: [
|
|
25436
25624
|
"bootstrap_project",
|
|
@@ -25689,12 +25877,8 @@ var init_token_utils = __esm(() => {
|
|
|
25689
25877
|
"delete_tag",
|
|
25690
25878
|
"get_label",
|
|
25691
25879
|
"get_activity_timeline",
|
|
25692
|
-
"get_recent_activity",
|
|
25693
25880
|
"get_tag",
|
|
25694
25881
|
"get_task_fields",
|
|
25695
|
-
"get_task_graph",
|
|
25696
|
-
"get_task_history",
|
|
25697
|
-
"get_task_stats",
|
|
25698
25882
|
"list_workflow_states",
|
|
25699
25883
|
"list_labels",
|
|
25700
25884
|
"list_tags",
|
|
@@ -25705,6 +25889,10 @@ var init_token_utils = __esm(() => {
|
|
|
25705
25889
|
"describe_tools",
|
|
25706
25890
|
"set_task_workflow_state",
|
|
25707
25891
|
"set_task_fields",
|
|
25892
|
+
"assign_label_to_task",
|
|
25893
|
+
"create_custom_field",
|
|
25894
|
+
"set_task_custom_field",
|
|
25895
|
+
"set_task_priority_meta",
|
|
25708
25896
|
"update_label",
|
|
25709
25897
|
"update_tag"
|
|
25710
25898
|
],
|
|
@@ -25730,7 +25918,6 @@ var init_token_utils = __esm(() => {
|
|
|
25730
25918
|
"update_template",
|
|
25731
25919
|
"write_template_library"
|
|
25732
25920
|
],
|
|
25733
|
-
webhooks: ["create_webhook", "delete_webhook", "list_webhooks"],
|
|
25734
25921
|
machines: [
|
|
25735
25922
|
"machines_archive",
|
|
25736
25923
|
"machines_delete",
|
|
@@ -27288,6 +27475,11 @@ function safeEqualHex(a, b) {
|
|
|
27288
27475
|
return false;
|
|
27289
27476
|
return timingSafeEqual3(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
|
|
27290
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
|
+
}
|
|
27291
27483
|
function generatePlaintextKey() {
|
|
27292
27484
|
return `tdos_${randomBytes2(32).toString("base64url")}`;
|
|
27293
27485
|
}
|
|
@@ -27895,6 +28087,37 @@ function parseBoundedLimit(value, fallback, max) {
|
|
|
27895
28087
|
return fallback;
|
|
27896
28088
|
return Math.min(parsed, max);
|
|
27897
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
|
+
}
|
|
27898
28121
|
function handleSseEvents(_req, url, ctx) {
|
|
27899
28122
|
const agentId = url.searchParams.get("agent_id") || undefined;
|
|
27900
28123
|
const projectId = url.searchParams.get("project_id") || undefined;
|
|
@@ -27973,35 +28196,40 @@ data: ${JSON.stringify({ type: "connected", agent_id: agentId, timestamp: new Da
|
|
|
27973
28196
|
});
|
|
27974
28197
|
}
|
|
27975
28198
|
function handleHealth(_ctx, json2) {
|
|
27976
|
-
const
|
|
27977
|
-
const
|
|
27978
|
-
const
|
|
27979
|
-
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
|
+
});
|
|
27980
28209
|
}
|
|
27981
28210
|
function handleHeadlessBoundary(_ctx, json2) {
|
|
27982
28211
|
const { getHeadlessBoundaryManifest: getHeadlessBoundaryManifest2 } = (init_headless_boundaries(), __toCommonJS(exports_headless_boundaries));
|
|
27983
28212
|
return json2(getHeadlessBoundaryManifest2());
|
|
27984
28213
|
}
|
|
27985
28214
|
function handleStats(_ctx, json2) {
|
|
27986
|
-
const
|
|
28215
|
+
const stats = getTaskStats();
|
|
28216
|
+
const byStatus = stats.by_status;
|
|
27987
28217
|
const projects = listProjects();
|
|
27988
28218
|
const agents = listAgents();
|
|
27989
|
-
const
|
|
27990
|
-
const
|
|
27991
|
-
const overdueRecurring = all.filter((t) => t.recurrence_rule && t.status === "pending" && t.due_at && t.due_at < nowStr).length;
|
|
27992
|
-
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;
|
|
27993
28221
|
return json2({
|
|
27994
|
-
total_tasks:
|
|
27995
|
-
pending:
|
|
27996
|
-
in_progress:
|
|
27997
|
-
completed:
|
|
27998
|
-
failed:
|
|
27999
|
-
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,
|
|
28000
28228
|
projects: projects.length,
|
|
28001
28229
|
agents: agents.length,
|
|
28002
|
-
stale_count:
|
|
28230
|
+
stale_count: staleCount,
|
|
28003
28231
|
overdue_recurring: overdueRecurring,
|
|
28004
|
-
recurring_tasks:
|
|
28232
|
+
recurring_tasks: countRecurringTasks()
|
|
28005
28233
|
});
|
|
28006
28234
|
}
|
|
28007
28235
|
async function handleListTasks(_req, url, _ctx, json2, taskToSummary2) {
|
|
@@ -28080,27 +28308,34 @@ function handleTasksExport(_req, url, _ctx, _json, taskToSummary2) {
|
|
|
28080
28308
|
const summaries = tasks.map((t) => taskToSummary2(t));
|
|
28081
28309
|
if (format === "csv") {
|
|
28082
28310
|
const headers = ["id", "short_id", "title", "status", "priority", "project_id", "assigned_to", "agent_id", "created_at", "updated_at", "completed_at", "due_at"];
|
|
28083
|
-
const
|
|
28084
|
-
const val = t[h];
|
|
28311
|
+
const csvCell = (val) => {
|
|
28085
28312
|
if (val === null || val === undefined)
|
|
28086
28313
|
return "";
|
|
28087
|
-
|
|
28088
|
-
|
|
28089
|
-
|
|
28090
|
-
|
|
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(","));
|
|
28091
28324
|
const csv = [headers.join(","), ...rows].join(`
|
|
28092
28325
|
`);
|
|
28093
28326
|
return new Response(csv, {
|
|
28094
28327
|
headers: {
|
|
28095
28328
|
"Content-Type": "text/csv",
|
|
28096
|
-
"Content-Disposition": "attachment; filename=tasks.csv"
|
|
28329
|
+
"Content-Disposition": "attachment; filename=tasks.csv",
|
|
28330
|
+
...SECURITY_HEADERS
|
|
28097
28331
|
}
|
|
28098
28332
|
});
|
|
28099
28333
|
}
|
|
28100
28334
|
return new Response(JSON.stringify(summaries, null, 2), {
|
|
28101
28335
|
headers: {
|
|
28102
28336
|
"Content-Type": "application/json",
|
|
28103
|
-
"Content-Disposition": "attachment; filename=tasks.json"
|
|
28337
|
+
"Content-Disposition": "attachment; filename=tasks.json",
|
|
28338
|
+
...SECURITY_HEADERS
|
|
28104
28339
|
}
|
|
28105
28340
|
});
|
|
28106
28341
|
}
|
|
@@ -28274,12 +28509,16 @@ async function handlePatchTask(id, req, _ctx, json2, taskToSummary2) {
|
|
|
28274
28509
|
if (ALLOWED.has(key))
|
|
28275
28510
|
safeBody[key] = value;
|
|
28276
28511
|
}
|
|
28512
|
+
const clientVersion = typeof body["version"] === "number" ? body["version"] : task.version;
|
|
28277
28513
|
const updated = updateTask(id, {
|
|
28278
28514
|
...safeBody,
|
|
28279
|
-
version:
|
|
28515
|
+
version: clientVersion
|
|
28280
28516
|
});
|
|
28281
28517
|
return json2(taskToSummary2(updated));
|
|
28282
28518
|
} catch (e) {
|
|
28519
|
+
const mapped = mapTaskError(e, json2);
|
|
28520
|
+
if (mapped)
|
|
28521
|
+
return mapped;
|
|
28283
28522
|
return json2({ error: e instanceof Error ? e.message : "Failed to update task" }, 500);
|
|
28284
28523
|
}
|
|
28285
28524
|
}
|
|
@@ -28295,6 +28534,9 @@ function handleStartTask(id, ctx, json2, taskToSummary2) {
|
|
|
28295
28534
|
ctx.broadcastEvent({ type: "task", task_id: task.id, action: "started", agent_id: "dashboard", project_id: task.project_id });
|
|
28296
28535
|
return json2(taskToSummary2(task));
|
|
28297
28536
|
} catch (e) {
|
|
28537
|
+
const mapped = mapTaskError(e, json2);
|
|
28538
|
+
if (mapped)
|
|
28539
|
+
return mapped;
|
|
28298
28540
|
return json2({ error: e instanceof Error ? e.message : "Failed to start task" }, 500);
|
|
28299
28541
|
}
|
|
28300
28542
|
}
|
|
@@ -28314,6 +28556,9 @@ function handleCompleteTask(id, ctx, json2, taskToSummary2) {
|
|
|
28314
28556
|
ctx.broadcastEvent({ type: "task", task_id: task.id, action: "completed", agent_id: "dashboard", project_id: task.project_id });
|
|
28315
28557
|
return json2(taskToSummary2(task));
|
|
28316
28558
|
} catch (e) {
|
|
28559
|
+
const mapped = mapTaskError(e, json2);
|
|
28560
|
+
if (mapped)
|
|
28561
|
+
return mapped;
|
|
28317
28562
|
return json2({ error: e instanceof Error ? e.message : "Failed to complete task" }, 500);
|
|
28318
28563
|
}
|
|
28319
28564
|
}
|
|
@@ -28638,6 +28883,8 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
|
|
|
28638
28883
|
}
|
|
28639
28884
|
var init_routes = __esm(() => {
|
|
28640
28885
|
init_tasks();
|
|
28886
|
+
init_database();
|
|
28887
|
+
init_types();
|
|
28641
28888
|
init_projects();
|
|
28642
28889
|
init_agents();
|
|
28643
28890
|
init_plans();
|
|
@@ -51837,14 +52084,16 @@ function printHelp() {
|
|
|
51837
52084
|
Start the @hasna/todos MCP server.
|
|
51838
52085
|
|
|
51839
52086
|
Options:
|
|
51840
|
-
--stdio Use stdio transport
|
|
51841
|
-
--
|
|
52087
|
+
--stdio Use stdio transport (default)
|
|
52088
|
+
--http Use Streamable HTTP transport
|
|
52089
|
+
--port <port> Use Streamable HTTP on the given port (implies --http)
|
|
51842
52090
|
-V, --version output the version number
|
|
51843
52091
|
-h, --help display help for command
|
|
51844
52092
|
|
|
51845
52093
|
Environment:
|
|
51846
|
-
|
|
51847
|
-
|
|
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
|
|
51848
52097
|
TODOS_PROFILE=<profile> Tool profile filter
|
|
51849
52098
|
TODOS_TOOL_GROUPS=<list> Comma-separated tool group filter`);
|
|
51850
52099
|
}
|
|
@@ -51933,8 +52182,22 @@ function formatError(error) {
|
|
|
51933
52182
|
function resolveId(partialId, table = "tasks") {
|
|
51934
52183
|
const db = getDatabase();
|
|
51935
52184
|
const id = resolvePartialId(db, table, partialId);
|
|
51936
|
-
if (!id)
|
|
51937
|
-
|
|
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
|
+
}
|
|
51938
52201
|
return id;
|
|
51939
52202
|
}
|
|
51940
52203
|
function formatTask(task2) {
|
|
@@ -52023,8 +52286,9 @@ function buildServer() {
|
|
|
52023
52286
|
return server;
|
|
52024
52287
|
}
|
|
52025
52288
|
async function main() {
|
|
52026
|
-
const {
|
|
52027
|
-
|
|
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) {
|
|
52028
52292
|
const server = buildServer();
|
|
52029
52293
|
const transport = new StdioServerTransport;
|
|
52030
52294
|
await server.connect(transport);
|
|
@@ -52200,7 +52464,7 @@ function checkAuth(req, apiKey) {
|
|
|
52200
52464
|
if (!apiKey && !generatedKeysEnabled)
|
|
52201
52465
|
return null;
|
|
52202
52466
|
const provided = getProvidedApiKey(req);
|
|
52203
|
-
const matchesEnvKey = Boolean(apiKey && provided && provided
|
|
52467
|
+
const matchesEnvKey = Boolean(apiKey && provided && safeEqualStrings(provided, apiKey));
|
|
52204
52468
|
const matchesGeneratedKey = Boolean(provided && verifyApiKey(provided));
|
|
52205
52469
|
if (!matchesEnvKey && !matchesGeneratedKey) {
|
|
52206
52470
|
return new Response(JSON.stringify({ error: "Unauthorized" }), {
|
|
@@ -52210,6 +52474,15 @@ function checkAuth(req, apiKey) {
|
|
|
52210
52474
|
}
|
|
52211
52475
|
return null;
|
|
52212
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
|
+
}
|
|
52213
52486
|
function checkRateLimit(ip) {
|
|
52214
52487
|
const now4 = Date.now();
|
|
52215
52488
|
const entry = rateLimitMap.get(ip);
|
|
@@ -52337,7 +52610,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
52337
52610
|
const server = Bun.serve({
|
|
52338
52611
|
port,
|
|
52339
52612
|
hostname: hostname3,
|
|
52340
|
-
async fetch(req) {
|
|
52613
|
+
async fetch(req, server2) {
|
|
52341
52614
|
const url = new URL(req.url);
|
|
52342
52615
|
const path = url.pathname;
|
|
52343
52616
|
const method = req.method;
|
|
@@ -52349,15 +52622,6 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
52349
52622
|
Vary: "Origin"
|
|
52350
52623
|
} : undefined;
|
|
52351
52624
|
const jsonWithCors = (data, status = 200) => json(data, status, corsHeaders);
|
|
52352
|
-
if (path === "/health" && method === "GET") {
|
|
52353
|
-
const { healthResponse: healthResponse2 } = await Promise.resolve().then(() => (init_http(), exports_http));
|
|
52354
|
-
return healthResponse2("todos");
|
|
52355
|
-
}
|
|
52356
|
-
if (path === "/mcp") {
|
|
52357
|
-
const { handleMcpHttpRequest: handleMcpHttpRequest2 } = await Promise.resolve().then(() => (init_http(), exports_http));
|
|
52358
|
-
const { buildServer: buildServer2 } = await Promise.resolve().then(() => (init_mcp2(), exports_mcp));
|
|
52359
|
-
return handleMcpHttpRequest2(req, buildServer2);
|
|
52360
|
-
}
|
|
52361
52625
|
if (method === "OPTIONS") {
|
|
52362
52626
|
return new Response(null, {
|
|
52363
52627
|
headers: corsHeaders || {
|
|
@@ -52365,7 +52629,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
52365
52629
|
}
|
|
52366
52630
|
});
|
|
52367
52631
|
}
|
|
52368
|
-
const ip = req
|
|
52632
|
+
const ip = resolveClientIp(req, server2);
|
|
52369
52633
|
const rl = checkRateLimit(ip);
|
|
52370
52634
|
if (!rl.allowed) {
|
|
52371
52635
|
return new Response(JSON.stringify({ error: "Too many requests", retry_after: rl.retryAfter }), {
|
|
@@ -52373,6 +52637,18 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
52373
52637
|
headers: { "Content-Type": "application/json", "Retry-After": String(rl.retryAfter ?? 60), ...SECURITY_HEADERS }
|
|
52374
52638
|
});
|
|
52375
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
|
+
}
|
|
52376
52652
|
if (path.startsWith("/api/")) {
|
|
52377
52653
|
const authError = checkAuth(req, apiKey);
|
|
52378
52654
|
if (authError)
|
|
@@ -55219,6 +55495,339 @@ var init_config_serve_commands = __esm(() => {
|
|
|
55219
55495
|
init_helpers();
|
|
55220
55496
|
});
|
|
55221
55497
|
|
|
55498
|
+
// src/lib/task-route-sources.ts
|
|
55499
|
+
var exports_task_route_sources = {};
|
|
55500
|
+
__export(exports_task_route_sources, {
|
|
55501
|
+
discoverTaskRouteSources: () => discoverTaskRouteSources,
|
|
55502
|
+
TASK_ROUTE_SOURCE_DISCOVERY_SCHEMA_VERSION: () => TASK_ROUTE_SOURCE_DISCOVERY_SCHEMA_VERSION
|
|
55503
|
+
});
|
|
55504
|
+
import { Database as Database3 } from "bun:sqlite";
|
|
55505
|
+
import { createHash as createHash13 } from "crypto";
|
|
55506
|
+
import { existsSync as existsSync21, readdirSync as readdirSync5, statSync as statSync9 } from "fs";
|
|
55507
|
+
import { basename as basename9, dirname as dirname12, join as join21, resolve as resolve21 } from "path";
|
|
55508
|
+
function normalizePath5(input) {
|
|
55509
|
+
return resolve21(input);
|
|
55510
|
+
}
|
|
55511
|
+
function sourceStoreId(sourceDbPath) {
|
|
55512
|
+
const digest = createHash13("sha256").update(sourceDbPath).digest("hex").slice(0, 16);
|
|
55513
|
+
return `sqlite:${digest}`;
|
|
55514
|
+
}
|
|
55515
|
+
function inferSourceRepoPath(sourceDbPath) {
|
|
55516
|
+
const normalized = normalizePath5(sourceDbPath);
|
|
55517
|
+
if (normalized.endsWith(TODO_STORE_RELATIVE_PATH)) {
|
|
55518
|
+
return dirname12(dirname12(dirname12(normalized)));
|
|
55519
|
+
}
|
|
55520
|
+
return dirname12(normalized);
|
|
55521
|
+
}
|
|
55522
|
+
function createStoreRef(sourceDbPath) {
|
|
55523
|
+
const normalized = normalizePath5(sourceDbPath);
|
|
55524
|
+
return {
|
|
55525
|
+
source_store_id: sourceStoreId(normalized),
|
|
55526
|
+
source_repo_path: inferSourceRepoPath(normalized),
|
|
55527
|
+
source_db_path: normalized
|
|
55528
|
+
};
|
|
55529
|
+
}
|
|
55530
|
+
function normalizePatterns(patterns) {
|
|
55531
|
+
return (patterns ?? []).map((pattern) => pattern.trim()).filter(Boolean);
|
|
55532
|
+
}
|
|
55533
|
+
function escapeRegExp(value) {
|
|
55534
|
+
return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
55535
|
+
}
|
|
55536
|
+
function globPatternToRegExp(pattern) {
|
|
55537
|
+
let source3 = "";
|
|
55538
|
+
for (const char of pattern) {
|
|
55539
|
+
if (char === "*")
|
|
55540
|
+
source3 += ".*";
|
|
55541
|
+
else if (char === "?")
|
|
55542
|
+
source3 += ".";
|
|
55543
|
+
else
|
|
55544
|
+
source3 += escapeRegExp(char);
|
|
55545
|
+
}
|
|
55546
|
+
return new RegExp(`^${source3}$`);
|
|
55547
|
+
}
|
|
55548
|
+
function matchesPattern4(value, pattern) {
|
|
55549
|
+
const normalizedValue = value.replace(/\\/g, "/");
|
|
55550
|
+
const normalizedPattern = pattern.replace(/\\/g, "/");
|
|
55551
|
+
if (normalizedPattern.includes("*") || normalizedPattern.includes("?")) {
|
|
55552
|
+
return globPatternToRegExp(normalizedPattern).test(normalizedValue);
|
|
55553
|
+
}
|
|
55554
|
+
return normalizedValue.includes(normalizedPattern);
|
|
55555
|
+
}
|
|
55556
|
+
function storeMatchesAny(ref, patterns) {
|
|
55557
|
+
if (patterns.length === 0)
|
|
55558
|
+
return false;
|
|
55559
|
+
const paths = [ref.source_db_path, ref.source_repo_path].filter((value) => Boolean(value));
|
|
55560
|
+
const values = paths.flatMap((value) => [value, basename9(value)]);
|
|
55561
|
+
return patterns.some((pattern) => values.some((value) => matchesPattern4(value, pattern)));
|
|
55562
|
+
}
|
|
55563
|
+
function shouldIncludeStore(ref, include, exclude) {
|
|
55564
|
+
const included = include.length === 0 || storeMatchesAny(ref, include);
|
|
55565
|
+
return included && !storeMatchesAny(ref, exclude);
|
|
55566
|
+
}
|
|
55567
|
+
function discoverStoresUnderRoot(sourceRoot) {
|
|
55568
|
+
const rootPath = normalizePath5(sourceRoot);
|
|
55569
|
+
const errors2 = [];
|
|
55570
|
+
const stores = [];
|
|
55571
|
+
if (!existsSync21(rootPath)) {
|
|
55572
|
+
const ref = createStoreRef(join21(rootPath, TODO_STORE_RELATIVE_PATH));
|
|
55573
|
+
errors2.push({
|
|
55574
|
+
...ref,
|
|
55575
|
+
code: "SOURCE_ROOT_MISSING",
|
|
55576
|
+
message: `Source root does not exist: ${rootPath}`
|
|
55577
|
+
});
|
|
55578
|
+
return { stores, errors: errors2 };
|
|
55579
|
+
}
|
|
55580
|
+
let rootStat;
|
|
55581
|
+
try {
|
|
55582
|
+
rootStat = statSync9(rootPath);
|
|
55583
|
+
} catch (error) {
|
|
55584
|
+
const ref = createStoreRef(join21(rootPath, TODO_STORE_RELATIVE_PATH));
|
|
55585
|
+
errors2.push({
|
|
55586
|
+
...ref,
|
|
55587
|
+
code: "SOURCE_ROOT_UNREADABLE",
|
|
55588
|
+
message: error instanceof Error ? error.message : `Unable to read source root: ${rootPath}`
|
|
55589
|
+
});
|
|
55590
|
+
return { stores, errors: errors2 };
|
|
55591
|
+
}
|
|
55592
|
+
if (rootStat.isFile()) {
|
|
55593
|
+
stores.push(createStoreRef(rootPath));
|
|
55594
|
+
return { stores, errors: errors2 };
|
|
55595
|
+
}
|
|
55596
|
+
function scanDirectory(dir, depth) {
|
|
55597
|
+
const candidate = join21(dir, TODO_STORE_RELATIVE_PATH);
|
|
55598
|
+
if (existsSync21(candidate)) {
|
|
55599
|
+
stores.push(createStoreRef(candidate));
|
|
55600
|
+
}
|
|
55601
|
+
if (depth >= ROOT_SCAN_MAX_DEPTH)
|
|
55602
|
+
return;
|
|
55603
|
+
let entries;
|
|
55604
|
+
try {
|
|
55605
|
+
entries = readdirSync5(dir, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
55606
|
+
} catch (error) {
|
|
55607
|
+
const ref = createStoreRef(candidate);
|
|
55608
|
+
errors2.push({
|
|
55609
|
+
...ref,
|
|
55610
|
+
code: "SOURCE_ROOT_UNREADABLE",
|
|
55611
|
+
message: error instanceof Error ? error.message : `Unable to read source root: ${dir}`
|
|
55612
|
+
});
|
|
55613
|
+
return;
|
|
55614
|
+
}
|
|
55615
|
+
for (const entry of entries) {
|
|
55616
|
+
if (!entry.isDirectory() || SKIPPED_SCAN_DIRS.has(entry.name))
|
|
55617
|
+
continue;
|
|
55618
|
+
scanDirectory(join21(dir, entry.name), depth + 1);
|
|
55619
|
+
}
|
|
55620
|
+
}
|
|
55621
|
+
scanDirectory(rootPath, 0);
|
|
55622
|
+
return { stores, errors: errors2 };
|
|
55623
|
+
}
|
|
55624
|
+
function collectStoreRefs(input) {
|
|
55625
|
+
const byPath = new Map;
|
|
55626
|
+
const errors2 = [];
|
|
55627
|
+
for (const storePath of input.sourceStores ?? []) {
|
|
55628
|
+
const ref = createStoreRef(storePath);
|
|
55629
|
+
byPath.set(ref.source_db_path, ref);
|
|
55630
|
+
}
|
|
55631
|
+
for (const sourceRoot of input.sourceRoots ?? []) {
|
|
55632
|
+
const discovered = discoverStoresUnderRoot(sourceRoot);
|
|
55633
|
+
for (const ref of discovered.stores) {
|
|
55634
|
+
byPath.set(ref.source_db_path, ref);
|
|
55635
|
+
}
|
|
55636
|
+
errors2.push(...discovered.errors);
|
|
55637
|
+
}
|
|
55638
|
+
return {
|
|
55639
|
+
stores: [...byPath.values()].sort((a, b) => a.source_db_path.localeCompare(b.source_db_path)),
|
|
55640
|
+
errors: errors2.sort((a, b) => a.source_db_path.localeCompare(b.source_db_path))
|
|
55641
|
+
};
|
|
55642
|
+
}
|
|
55643
|
+
function openReadonlyStore(ref) {
|
|
55644
|
+
if (!existsSync21(ref.source_db_path)) {
|
|
55645
|
+
throw Object.assign(new Error(`Store does not exist: ${ref.source_db_path}`), { code: "STORE_MISSING" });
|
|
55646
|
+
}
|
|
55647
|
+
return new Database3(ref.source_db_path, { readonly: true, create: false });
|
|
55648
|
+
}
|
|
55649
|
+
function hasTable2(db, tableName) {
|
|
55650
|
+
const row = db.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(tableName);
|
|
55651
|
+
return Boolean(row);
|
|
55652
|
+
}
|
|
55653
|
+
function tableColumns2(db, tableName) {
|
|
55654
|
+
const rows = db.query(`PRAGMA table_info(${tableName})`).all();
|
|
55655
|
+
return new Set(rows.map((row) => row.name));
|
|
55656
|
+
}
|
|
55657
|
+
function listPendingTasksReadonly(db) {
|
|
55658
|
+
if (!hasTable2(db, "tasks")) {
|
|
55659
|
+
throw Object.assign(new Error("Store does not contain a tasks table"), { code: "STORE_INVALID" });
|
|
55660
|
+
}
|
|
55661
|
+
const columns = tableColumns2(db, "tasks");
|
|
55662
|
+
const conditions = ["status = 'pending'"];
|
|
55663
|
+
if (columns.has("archived_at"))
|
|
55664
|
+
conditions.push("archived_at IS NULL");
|
|
55665
|
+
const rows = db.query(`SELECT * FROM tasks WHERE ${conditions.join(" AND ")}
|
|
55666
|
+
ORDER BY CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END, created_at DESC`).all();
|
|
55667
|
+
return rows.map(rowToTask);
|
|
55668
|
+
}
|
|
55669
|
+
function isReadyTask(task2, db) {
|
|
55670
|
+
if (task2.locked_by && !isLockExpired(task2.locked_at))
|
|
55671
|
+
return false;
|
|
55672
|
+
return getBlockingDeps(task2.id, db).length === 0;
|
|
55673
|
+
}
|
|
55674
|
+
function metadataFingerprint(metadata) {
|
|
55675
|
+
const value = metadata.fingerprint;
|
|
55676
|
+
if (typeof value === "string" && value.trim())
|
|
55677
|
+
return value;
|
|
55678
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
55679
|
+
return String(value);
|
|
55680
|
+
return null;
|
|
55681
|
+
}
|
|
55682
|
+
function boundedMetadataValue(value, depth = 0) {
|
|
55683
|
+
if (depth > 6)
|
|
55684
|
+
return "[TRUNCATED]";
|
|
55685
|
+
if (typeof value === "string") {
|
|
55686
|
+
return value.length > 2000 ? `${value.slice(0, 2000)}[TRUNCATED]` : value;
|
|
55687
|
+
}
|
|
55688
|
+
if (Array.isArray(value)) {
|
|
55689
|
+
return value.slice(0, 50).map((item) => boundedMetadataValue(item, depth + 1));
|
|
55690
|
+
}
|
|
55691
|
+
if (value && typeof value === "object") {
|
|
55692
|
+
const result = {};
|
|
55693
|
+
for (const [key, child] of Object.entries(value).slice(0, 80)) {
|
|
55694
|
+
const normalized = key.toLowerCase();
|
|
55695
|
+
if (normalized === "comment" || normalized === "comments" || normalized === "task_comments") {
|
|
55696
|
+
result[key] = "[REDACTED_COMMENT]";
|
|
55697
|
+
continue;
|
|
55698
|
+
}
|
|
55699
|
+
result[key] = boundedMetadataValue(child, depth + 1);
|
|
55700
|
+
}
|
|
55701
|
+
return result;
|
|
55702
|
+
}
|
|
55703
|
+
return value;
|
|
55704
|
+
}
|
|
55705
|
+
function discoveryMetadata(metadata) {
|
|
55706
|
+
return redactValue(boundedMetadataValue(metadata));
|
|
55707
|
+
}
|
|
55708
|
+
function sourceCandidate(ref, task2, db) {
|
|
55709
|
+
const routeState = getTaskRouteState(task2, db);
|
|
55710
|
+
const autoRoute = task2.tags.includes("auto:route") || task2.tags.includes("route:enabled");
|
|
55711
|
+
return {
|
|
55712
|
+
source_store_id: ref.source_store_id,
|
|
55713
|
+
source_repo_path: ref.source_repo_path,
|
|
55714
|
+
source_db_path: ref.source_db_path,
|
|
55715
|
+
source_task_key: `${ref.source_store_id}:${task2.id}`,
|
|
55716
|
+
source_selected_by_input: true,
|
|
55717
|
+
task_id: task2.id,
|
|
55718
|
+
task_short_id: task2.short_id,
|
|
55719
|
+
title: task2.title,
|
|
55720
|
+
status: task2.status,
|
|
55721
|
+
priority: task2.priority,
|
|
55722
|
+
project_path: routeState.route.project_path ?? task2.working_dir ?? ref.source_repo_path,
|
|
55723
|
+
task_version: task2.version,
|
|
55724
|
+
task_updated_at: task2.updated_at,
|
|
55725
|
+
task_fingerprint: metadataFingerprint(task2.metadata),
|
|
55726
|
+
tags: task2.tags,
|
|
55727
|
+
task_intent: {
|
|
55728
|
+
auto_route: autoRoute
|
|
55729
|
+
},
|
|
55730
|
+
metadata: discoveryMetadata(task2.metadata),
|
|
55731
|
+
route_state: routeState
|
|
55732
|
+
};
|
|
55733
|
+
}
|
|
55734
|
+
function discoveryError(ref, code, error) {
|
|
55735
|
+
return {
|
|
55736
|
+
...ref,
|
|
55737
|
+
code,
|
|
55738
|
+
message: error instanceof Error ? error.message : String(error)
|
|
55739
|
+
};
|
|
55740
|
+
}
|
|
55741
|
+
function errorCode(error) {
|
|
55742
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "STORE_MISSING") {
|
|
55743
|
+
return "STORE_MISSING";
|
|
55744
|
+
}
|
|
55745
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "STORE_INVALID") {
|
|
55746
|
+
return "STORE_INVALID";
|
|
55747
|
+
}
|
|
55748
|
+
return "STORE_UNREADABLE";
|
|
55749
|
+
}
|
|
55750
|
+
function discoverTaskRouteSources(input) {
|
|
55751
|
+
const include = normalizePatterns(input.include);
|
|
55752
|
+
const exclude = normalizePatterns(input.exclude);
|
|
55753
|
+
const sourceRoots = (input.sourceRoots ?? []).map(normalizePath5).sort();
|
|
55754
|
+
const sourceStores = (input.sourceStores ?? []).map(normalizePath5).sort();
|
|
55755
|
+
const limit = Number.isFinite(input.limit ?? NaN) && (input.limit ?? 0) >= 0 ? Math.floor(input.limit ?? 0) : null;
|
|
55756
|
+
const collected = collectStoreRefs(input);
|
|
55757
|
+
const stores = [];
|
|
55758
|
+
const errors2 = [...collected.errors];
|
|
55759
|
+
const candidates = [];
|
|
55760
|
+
let totalCandidateCount = 0;
|
|
55761
|
+
for (const ref of collected.stores) {
|
|
55762
|
+
if (!shouldIncludeStore(ref, include, exclude))
|
|
55763
|
+
continue;
|
|
55764
|
+
const storeErrors = [];
|
|
55765
|
+
let db = null;
|
|
55766
|
+
try {
|
|
55767
|
+
db = openReadonlyStore(ref);
|
|
55768
|
+
const readyTasks = listPendingTasksReadonly(db).filter((task2) => isReadyTask(task2, db));
|
|
55769
|
+
totalCandidateCount += readyTasks.length;
|
|
55770
|
+
const remaining = limit === null ? readyTasks.length : Math.max(0, limit - candidates.length);
|
|
55771
|
+
const selectedTasks = limit === null ? readyTasks : readyTasks.slice(0, remaining);
|
|
55772
|
+
candidates.push(...selectedTasks.map((task2) => sourceCandidate(ref, task2, db)));
|
|
55773
|
+
stores.push({
|
|
55774
|
+
...ref,
|
|
55775
|
+
status: "ok",
|
|
55776
|
+
candidate_count: readyTasks.length,
|
|
55777
|
+
returned_candidate_count: selectedTasks.length,
|
|
55778
|
+
errors: []
|
|
55779
|
+
});
|
|
55780
|
+
} catch (error) {
|
|
55781
|
+
const storeError = discoveryError(ref, errorCode(error), error);
|
|
55782
|
+
storeErrors.push(storeError);
|
|
55783
|
+
errors2.push(storeError);
|
|
55784
|
+
stores.push({
|
|
55785
|
+
...ref,
|
|
55786
|
+
status: storeError.code === "STORE_MISSING" ? "missing" : "error",
|
|
55787
|
+
candidate_count: 0,
|
|
55788
|
+
returned_candidate_count: 0,
|
|
55789
|
+
errors: storeErrors
|
|
55790
|
+
});
|
|
55791
|
+
} finally {
|
|
55792
|
+
db?.close();
|
|
55793
|
+
}
|
|
55794
|
+
}
|
|
55795
|
+
return {
|
|
55796
|
+
schema_version: TASK_ROUTE_SOURCE_DISCOVERY_SCHEMA_VERSION,
|
|
55797
|
+
sourceRoots,
|
|
55798
|
+
sourceStores,
|
|
55799
|
+
include,
|
|
55800
|
+
exclude,
|
|
55801
|
+
limit,
|
|
55802
|
+
total_candidate_count: totalCandidateCount,
|
|
55803
|
+
returned_candidate_count: candidates.length,
|
|
55804
|
+
truncated: limit !== null && totalCandidateCount > candidates.length,
|
|
55805
|
+
stores,
|
|
55806
|
+
candidates,
|
|
55807
|
+
errors: errors2
|
|
55808
|
+
};
|
|
55809
|
+
}
|
|
55810
|
+
var TASK_ROUTE_SOURCE_DISCOVERY_SCHEMA_VERSION = "todos.task_route_sources.v1", TODO_STORE_RELATIVE_PATH, ROOT_SCAN_MAX_DEPTH = 5, SKIPPED_SCAN_DIRS;
|
|
55811
|
+
var init_task_route_sources = __esm(() => {
|
|
55812
|
+
init_database();
|
|
55813
|
+
init_task_lifecycle();
|
|
55814
|
+
init_task_crud();
|
|
55815
|
+
init_redaction();
|
|
55816
|
+
init_task_routing();
|
|
55817
|
+
TODO_STORE_RELATIVE_PATH = join21(".hasna", "todos", "todos.db");
|
|
55818
|
+
SKIPPED_SCAN_DIRS = new Set([
|
|
55819
|
+
".git",
|
|
55820
|
+
".hg",
|
|
55821
|
+
".svn",
|
|
55822
|
+
"node_modules",
|
|
55823
|
+
"dist",
|
|
55824
|
+
"build",
|
|
55825
|
+
".next",
|
|
55826
|
+
".turbo",
|
|
55827
|
+
".cache"
|
|
55828
|
+
]);
|
|
55829
|
+
});
|
|
55830
|
+
|
|
55222
55831
|
// src/lib/tester-issue-reports.ts
|
|
55223
55832
|
var exports_tester_issue_reports = {};
|
|
55224
55833
|
__export(exports_tester_issue_reports, {
|
|
@@ -55231,7 +55840,7 @@ __export(exports_tester_issue_reports, {
|
|
|
55231
55840
|
TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION: () => TESTERS_ISSUE_REPORT_RESULT_SCHEMA_VERSION,
|
|
55232
55841
|
TESTERS_ISSUE_REPORT_BATCH_RESULT_SCHEMA_VERSION: () => TESTERS_ISSUE_REPORT_BATCH_RESULT_SCHEMA_VERSION
|
|
55233
55842
|
});
|
|
55234
|
-
import { createHash as
|
|
55843
|
+
import { createHash as createHash14 } from "crypto";
|
|
55235
55844
|
function asObject3(value) {
|
|
55236
55845
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
55237
55846
|
}
|
|
@@ -55403,7 +56012,7 @@ function fingerprintTesterIssueReport(report) {
|
|
|
55403
56012
|
normalizeText4(report.failure?.message || report.summary || report.title).slice(0, 240),
|
|
55404
56013
|
normalizeText4(stackTop).slice(0, 160)
|
|
55405
56014
|
].join("::");
|
|
55406
|
-
return `testers:${
|
|
56015
|
+
return `testers:${createHash14("sha256").update(raw).digest("hex").slice(0, 16)}`;
|
|
55407
56016
|
}
|
|
55408
56017
|
function priorityForSeverity(severity, fallback) {
|
|
55409
56018
|
return PRIORITIES5.includes(severity) ? severity : fallback;
|
|
@@ -55722,6 +56331,15 @@ function parseCsvOption(value) {
|
|
|
55722
56331
|
const values = value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
55723
56332
|
return values.length > 0 ? values : undefined;
|
|
55724
56333
|
}
|
|
56334
|
+
function collectOption2(value, previous = []) {
|
|
56335
|
+
return [...previous, value];
|
|
56336
|
+
}
|
|
56337
|
+
function expandRepeatedCsvOption(value) {
|
|
56338
|
+
if (!value || value.length === 0)
|
|
56339
|
+
return;
|
|
56340
|
+
const values = value.flatMap((item) => item.split(",").map((part) => part.trim()).filter(Boolean));
|
|
56341
|
+
return values.length > 0 ? values : undefined;
|
|
56342
|
+
}
|
|
55725
56343
|
function resolveOptionalId(table, value) {
|
|
55726
56344
|
if (!value)
|
|
55727
56345
|
return;
|
|
@@ -55880,6 +56498,7 @@ function registerQueryCommands(program2) {
|
|
|
55880
56498
|
});
|
|
55881
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) => {
|
|
55882
56500
|
const globalOpts = program2.opts();
|
|
56501
|
+
const json2 = opts.json || globalOpts.json;
|
|
55883
56502
|
const db = getDatabase();
|
|
55884
56503
|
const filters = {};
|
|
55885
56504
|
const projectInput = opts.project || globalOpts.project;
|
|
@@ -55890,10 +56509,14 @@ function registerQueryCommands(program2) {
|
|
|
55890
56509
|
}
|
|
55891
56510
|
const task2 = getNextTask(opts.agent, Object.keys(filters).length ? filters : undefined, db);
|
|
55892
56511
|
if (!task2) {
|
|
56512
|
+
if (json2) {
|
|
56513
|
+
console.log(JSON.stringify(null));
|
|
56514
|
+
return;
|
|
56515
|
+
}
|
|
55893
56516
|
console.log(chalk7.dim("No tasks available."));
|
|
55894
56517
|
return;
|
|
55895
56518
|
}
|
|
55896
|
-
if (
|
|
56519
|
+
if (json2) {
|
|
55897
56520
|
console.log(JSON.stringify(task2, null, 2));
|
|
55898
56521
|
return;
|
|
55899
56522
|
}
|
|
@@ -55903,16 +56526,26 @@ function registerQueryCommands(program2) {
|
|
|
55903
56526
|
console.log(chalk7.dim(` ${task2.description.slice(0, 100)}`));
|
|
55904
56527
|
});
|
|
55905
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;
|
|
55906
56531
|
const db = getDatabase();
|
|
55907
56532
|
const filters = {};
|
|
55908
|
-
|
|
55909
|
-
|
|
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
|
+
}
|
|
55910
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);
|
|
55911
56540
|
if (!task2) {
|
|
56541
|
+
if (json2) {
|
|
56542
|
+
console.log(JSON.stringify(null));
|
|
56543
|
+
return;
|
|
56544
|
+
}
|
|
55912
56545
|
console.log(chalk7.dim("No tasks available to claim."));
|
|
55913
56546
|
return;
|
|
55914
56547
|
}
|
|
55915
|
-
if (
|
|
56548
|
+
if (json2) {
|
|
55916
56549
|
console.log(JSON.stringify(task2, null, 2));
|
|
55917
56550
|
return;
|
|
55918
56551
|
}
|
|
@@ -55933,12 +56566,14 @@ function registerQueryCommands(program2) {
|
|
|
55933
56566
|
console.log(chalk7.green(`Stolen: ${task2.short_id || task2.id.slice(0, 8)} | ${task2.priority} | ${task2.title}`));
|
|
55934
56567
|
});
|
|
55935
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;
|
|
55936
56571
|
const db = getDatabase();
|
|
55937
56572
|
const filters = {};
|
|
55938
56573
|
if (opts.project)
|
|
55939
56574
|
filters.project_id = opts.project;
|
|
55940
56575
|
const s = getStatus(Object.keys(filters).length ? filters : undefined, opts.agent, undefined, db);
|
|
55941
|
-
if (
|
|
56576
|
+
if (json2) {
|
|
55942
56577
|
console.log(JSON.stringify(s, null, 2));
|
|
55943
56578
|
return;
|
|
55944
56579
|
}
|
|
@@ -56075,6 +56710,8 @@ Blocked:`));
|
|
|
56075
56710
|
console.log();
|
|
56076
56711
|
});
|
|
56077
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;
|
|
56078
56715
|
const db = getDatabase();
|
|
56079
56716
|
const resolvedId = resolvePartialId(db, "tasks", id);
|
|
56080
56717
|
if (!resolvedId) {
|
|
@@ -56082,7 +56719,7 @@ Blocked:`));
|
|
|
56082
56719
|
process.exit(1);
|
|
56083
56720
|
}
|
|
56084
56721
|
const result = failTask(resolvedId, opts.agent, opts.reason, { retry: opts.retry }, db);
|
|
56085
|
-
if (
|
|
56722
|
+
if (json2) {
|
|
56086
56723
|
console.log(JSON.stringify(result, null, 2));
|
|
56087
56724
|
return;
|
|
56088
56725
|
}
|
|
@@ -56093,12 +56730,14 @@ Blocked:`));
|
|
|
56093
56730
|
console.log(chalk7.yellow(`Retry created: ${result.retryTask.short_id || result.retryTask.id.slice(0, 8)} | ${result.retryTask.title}`));
|
|
56094
56731
|
});
|
|
56095
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;
|
|
56096
56735
|
const db = getDatabase();
|
|
56097
56736
|
const filters = {};
|
|
56098
56737
|
if (opts.project)
|
|
56099
56738
|
filters.project_id = opts.project;
|
|
56100
56739
|
const work = getActiveWork(Object.keys(filters).length ? filters : undefined, db);
|
|
56101
|
-
if (
|
|
56740
|
+
if (json2) {
|
|
56102
56741
|
console.log(JSON.stringify(work, null, 2));
|
|
56103
56742
|
return;
|
|
56104
56743
|
}
|
|
@@ -56114,12 +56753,14 @@ Blocked:`));
|
|
|
56114
56753
|
}
|
|
56115
56754
|
});
|
|
56116
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;
|
|
56117
56758
|
const db = getDatabase();
|
|
56118
56759
|
const filters = {};
|
|
56119
56760
|
if (opts.project)
|
|
56120
56761
|
filters.project_id = opts.project;
|
|
56121
56762
|
const tasks = getStaleTasks(parseInt(opts.minutes, 10), Object.keys(filters).length ? filters : undefined, db);
|
|
56122
|
-
if (
|
|
56763
|
+
if (json2) {
|
|
56123
56764
|
console.log(JSON.stringify(tasks, null, 2));
|
|
56124
56765
|
return;
|
|
56125
56766
|
}
|
|
@@ -56143,7 +56784,7 @@ Blocked:`));
|
|
|
56143
56784
|
project_id: projectId,
|
|
56144
56785
|
limit: opts.limit ? parseInt(opts.limit, 10) : undefined
|
|
56145
56786
|
}, db);
|
|
56146
|
-
if (opts.json) {
|
|
56787
|
+
if (opts.json || globalOpts.json) {
|
|
56147
56788
|
console.log(JSON.stringify(result, null, 2));
|
|
56148
56789
|
return;
|
|
56149
56790
|
}
|
|
@@ -56360,13 +57001,13 @@ Repairs`));
|
|
|
56360
57001
|
try {
|
|
56361
57002
|
const db = getDatabase();
|
|
56362
57003
|
const row = db.query("SELECT COUNT(*) as count FROM tasks").get();
|
|
56363
|
-
const { statSync:
|
|
56364
|
-
const { join:
|
|
57004
|
+
const { statSync: statSync10 } = await import("fs");
|
|
57005
|
+
const { join: join22 } = await import("path");
|
|
56365
57006
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
56366
|
-
const dbPath = process.env["HASNA_TODOS_DB_PATH"] || process.env["TODOS_DB_PATH"] ||
|
|
57007
|
+
const dbPath = process.env["HASNA_TODOS_DB_PATH"] || process.env["TODOS_DB_PATH"] || join22(home, ".hasna", "todos", "todos.db");
|
|
56367
57008
|
let size = "unknown";
|
|
56368
57009
|
try {
|
|
56369
|
-
size = `${(
|
|
57010
|
+
size = `${(statSync10(dbPath).size / 1024 / 1024).toFixed(1)} MB`;
|
|
56370
57011
|
} catch {}
|
|
56371
57012
|
checks.push({ name: "Database", ok: true, message: `${row.count} tasks \xB7 ${size} \xB7 ${chalk7.dim(dbPath)}` });
|
|
56372
57013
|
} catch (e) {
|
|
@@ -56856,8 +57497,42 @@ Repairs`));
|
|
|
56856
57497
|
console.log(` ${chalk7.dim(time2)} ${chalk7.cyan(entry.source)} ${chalk7.dim(ref)} ${entry.event_type}${message}${agent}`);
|
|
56857
57498
|
}
|
|
56858
57499
|
});
|
|
56859
|
-
program2.command("ready").description("Show all tasks ready to be claimed (pending, unblocked, unlocked)").option("-j, --json", "Output as JSON").option("--project <id>", "Filter to project").option("--limit <n>", "Max tasks to show", "20").action(async (opts) => {
|
|
57500
|
+
program2.command("ready").description("Show all tasks ready to be claimed (pending, unblocked, unlocked)").option("-j, --json", "Output as JSON").option("--project <id>", "Filter to project").option("--limit <n>", "Max tasks to show", "20").option("--source-root <path>", "Read-only source root to scan for .hasna/todos/todos.db (repeatable)", collectOption2, []).option("--source-store <path>", "Read-only todos SQLite store path to scan (repeatable)", collectOption2, []).option("--include <pattern>", "Include source repo/store paths matching substring or glob (repeatable or comma-separated)", collectOption2, []).option("--exclude <pattern>", "Exclude source repo/store paths matching substring or glob (repeatable or comma-separated)", collectOption2, []).action(async (opts) => {
|
|
56860
57501
|
const globalOpts = program2.opts();
|
|
57502
|
+
const sourceRoots = expandRepeatedCsvOption(opts.sourceRoot);
|
|
57503
|
+
const sourceStores = expandRepeatedCsvOption(opts.sourceStore);
|
|
57504
|
+
const include = expandRepeatedCsvOption(opts.include);
|
|
57505
|
+
const exclude = expandRepeatedCsvOption(opts.exclude);
|
|
57506
|
+
if (sourceRoots || sourceStores || include || exclude) {
|
|
57507
|
+
const { discoverTaskRouteSources: discoverTaskRouteSources2 } = await Promise.resolve().then(() => (init_task_route_sources(), exports_task_route_sources));
|
|
57508
|
+
const result = discoverTaskRouteSources2({
|
|
57509
|
+
sourceRoots,
|
|
57510
|
+
sourceStores,
|
|
57511
|
+
include,
|
|
57512
|
+
exclude,
|
|
57513
|
+
limit: parseInt(opts.limit, 10)
|
|
57514
|
+
});
|
|
57515
|
+
if (opts.json || globalOpts.json) {
|
|
57516
|
+
console.log(JSON.stringify(result));
|
|
57517
|
+
return;
|
|
57518
|
+
}
|
|
57519
|
+
if (result.candidates.length === 0) {
|
|
57520
|
+
console.log(chalk7.dim(" No source tasks ready to claim."));
|
|
57521
|
+
} else {
|
|
57522
|
+
console.log(chalk7.bold(`Ready source tasks (${result.candidates.length}):
|
|
57523
|
+
`));
|
|
57524
|
+
for (const candidate of result.candidates) {
|
|
57525
|
+
const source3 = candidate.source_repo_path ?? candidate.source_db_path;
|
|
57526
|
+
const pri = candidate.priority === "critical" ? chalk7.bgRed.white(" CRIT ") : candidate.priority === "high" ? chalk7.red("[high]") : candidate.priority === "medium" ? chalk7.yellow("[med]") : "";
|
|
57527
|
+
console.log(` ${chalk7.cyan(candidate.task_short_id || candidate.task_id.slice(0, 8))} ${candidate.title} ${pri}${chalk7.dim(` ${source3}`)}`);
|
|
57528
|
+
}
|
|
57529
|
+
}
|
|
57530
|
+
if (result.errors.length > 0) {
|
|
57531
|
+
console.log(chalk7.yellow(`
|
|
57532
|
+
${result.errors.length} source error${result.errors.length === 1 ? "" : "s"} isolated; rerun with --json for details.`));
|
|
57533
|
+
}
|
|
57534
|
+
return;
|
|
57535
|
+
}
|
|
56861
57536
|
const db = getDatabase();
|
|
56862
57537
|
const { getBlockingDeps: getBlockingDeps2 } = await Promise.resolve().then(() => (init_tasks(), exports_tasks));
|
|
56863
57538
|
const { isLockExpired: isLockExpired2 } = await Promise.resolve().then(() => (init_database(), exports_database));
|
|
@@ -58179,21 +58854,21 @@ __export(exports_mcp_hooks_commands, {
|
|
|
58179
58854
|
});
|
|
58180
58855
|
import chalk8 from "chalk";
|
|
58181
58856
|
import { execSync as execSync3 } from "child_process";
|
|
58182
|
-
import { existsSync as
|
|
58183
|
-
import { dirname as
|
|
58857
|
+
import { existsSync as existsSync22, readFileSync as readFileSync17, writeFileSync as writeFileSync10, mkdirSync as mkdirSync11, chmodSync as chmodSync2 } from "fs";
|
|
58858
|
+
import { dirname as dirname13, join as join22 } from "path";
|
|
58184
58859
|
function getMcpBinaryPath() {
|
|
58185
58860
|
try {
|
|
58186
58861
|
const p = execSync3("which todos-mcp", { encoding: "utf-8" }).trim();
|
|
58187
58862
|
if (p)
|
|
58188
58863
|
return p;
|
|
58189
58864
|
} catch {}
|
|
58190
|
-
const bunBin =
|
|
58191
|
-
if (
|
|
58865
|
+
const bunBin = join22(HOME2, ".bun", "bin", "todos-mcp");
|
|
58866
|
+
if (existsSync22(bunBin))
|
|
58192
58867
|
return bunBin;
|
|
58193
58868
|
return "todos-mcp";
|
|
58194
58869
|
}
|
|
58195
58870
|
function readJsonFile2(path) {
|
|
58196
|
-
if (!
|
|
58871
|
+
if (!existsSync22(path))
|
|
58197
58872
|
return {};
|
|
58198
58873
|
try {
|
|
58199
58874
|
return JSON.parse(readFileSync17(path, "utf-8"));
|
|
@@ -58202,20 +58877,20 @@ function readJsonFile2(path) {
|
|
|
58202
58877
|
}
|
|
58203
58878
|
}
|
|
58204
58879
|
function writeJsonFile2(path, data) {
|
|
58205
|
-
const dir =
|
|
58206
|
-
if (!
|
|
58880
|
+
const dir = dirname13(path);
|
|
58881
|
+
if (!existsSync22(dir))
|
|
58207
58882
|
mkdirSync11(dir, { recursive: true });
|
|
58208
58883
|
writeFileSync10(path, JSON.stringify(data, null, 2) + `
|
|
58209
58884
|
`);
|
|
58210
58885
|
}
|
|
58211
58886
|
function readTomlFile(path) {
|
|
58212
|
-
if (!
|
|
58887
|
+
if (!existsSync22(path))
|
|
58213
58888
|
return "";
|
|
58214
58889
|
return readFileSync17(path, "utf-8");
|
|
58215
58890
|
}
|
|
58216
58891
|
function writeTomlFile(path, content) {
|
|
58217
|
-
const dir =
|
|
58218
|
-
if (!
|
|
58892
|
+
const dir = dirname13(path);
|
|
58893
|
+
if (!existsSync22(dir))
|
|
58219
58894
|
mkdirSync11(dir, { recursive: true });
|
|
58220
58895
|
writeFileSync10(path, content);
|
|
58221
58896
|
}
|
|
@@ -58262,7 +58937,7 @@ function envOption(value) {
|
|
|
58262
58937
|
}
|
|
58263
58938
|
function registerClaude(binPath, global) {
|
|
58264
58939
|
const scope = global ? "user" : "project";
|
|
58265
|
-
const cmd = `claude mcp add --transport stdio --scope ${scope} todos -- ${binPath}`;
|
|
58940
|
+
const cmd = `claude mcp add --transport stdio --scope ${scope} todos -- ${binPath} --stdio`;
|
|
58266
58941
|
try {
|
|
58267
58942
|
execSync3(cmd, { stdio: "pipe" });
|
|
58268
58943
|
console.log(chalk8.green(`Claude Code (${scope}): registered via 'claude mcp add'`));
|
|
@@ -58281,13 +58956,13 @@ function unregisterClaude(_global) {
|
|
|
58281
58956
|
}
|
|
58282
58957
|
}
|
|
58283
58958
|
function registerCodex(binPath) {
|
|
58284
|
-
const configPath =
|
|
58959
|
+
const configPath = join22(HOME2, ".codex", "config.toml");
|
|
58285
58960
|
let content = readTomlFile(configPath);
|
|
58286
58961
|
content = removeTomlBlock(content, "mcp_servers.todos");
|
|
58287
58962
|
const block = `
|
|
58288
58963
|
[mcp_servers.todos]
|
|
58289
58964
|
command = "${binPath}"
|
|
58290
|
-
args = []
|
|
58965
|
+
args = ["--stdio"]
|
|
58291
58966
|
`;
|
|
58292
58967
|
content = content.trimEnd() + `
|
|
58293
58968
|
` + block;
|
|
@@ -58295,7 +58970,7 @@ args = []
|
|
|
58295
58970
|
console.log(chalk8.green(`Codex CLI: registered in ${configPath}`));
|
|
58296
58971
|
}
|
|
58297
58972
|
function unregisterCodex() {
|
|
58298
|
-
const configPath =
|
|
58973
|
+
const configPath = join22(HOME2, ".codex", "config.toml");
|
|
58299
58974
|
let content = readTomlFile(configPath);
|
|
58300
58975
|
if (!content.includes("[mcp_servers.todos]")) {
|
|
58301
58976
|
console.log(chalk8.dim(`Codex CLI: todos not found in ${configPath}`));
|
|
@@ -58307,7 +58982,7 @@ function unregisterCodex() {
|
|
|
58307
58982
|
console.log(chalk8.green(`Codex CLI: unregistered from ${configPath}`));
|
|
58308
58983
|
}
|
|
58309
58984
|
function registerGemini(binPath) {
|
|
58310
|
-
const configPath =
|
|
58985
|
+
const configPath = join22(HOME2, ".gemini", "settings.json");
|
|
58311
58986
|
const config = readJsonFile2(configPath);
|
|
58312
58987
|
if (!config["mcpServers"]) {
|
|
58313
58988
|
config["mcpServers"] = {};
|
|
@@ -58315,13 +58990,13 @@ function registerGemini(binPath) {
|
|
|
58315
58990
|
const servers = config["mcpServers"];
|
|
58316
58991
|
servers["todos"] = {
|
|
58317
58992
|
command: binPath,
|
|
58318
|
-
args: []
|
|
58993
|
+
args: ["--stdio"]
|
|
58319
58994
|
};
|
|
58320
58995
|
writeJsonFile2(configPath, config);
|
|
58321
58996
|
console.log(chalk8.green(`Gemini CLI: registered in ${configPath}`));
|
|
58322
58997
|
}
|
|
58323
58998
|
function unregisterGemini() {
|
|
58324
|
-
const configPath =
|
|
58999
|
+
const configPath = join22(HOME2, ".gemini", "settings.json");
|
|
58325
59000
|
const config = readJsonFile2(configPath);
|
|
58326
59001
|
const servers = config["mcpServers"];
|
|
58327
59002
|
if (!servers || !("todos" in servers)) {
|
|
@@ -58378,8 +59053,8 @@ function registerMcpHooksCommands(program2) {
|
|
|
58378
59053
|
if (p)
|
|
58379
59054
|
todosBin = p;
|
|
58380
59055
|
} catch {}
|
|
58381
|
-
const hooksDir =
|
|
58382
|
-
if (!
|
|
59056
|
+
const hooksDir = join22(process.cwd(), ".claude", "hooks");
|
|
59057
|
+
if (!existsSync22(hooksDir))
|
|
58383
59058
|
mkdirSync11(hooksDir, { recursive: true });
|
|
58384
59059
|
const hookScript = `#!/usr/bin/env bash
|
|
58385
59060
|
# Auto-generated by: todos hooks install
|
|
@@ -58404,11 +59079,11 @@ esac
|
|
|
58404
59079
|
|
|
58405
59080
|
exit 0
|
|
58406
59081
|
`;
|
|
58407
|
-
const hookPath =
|
|
59082
|
+
const hookPath = join22(hooksDir, "todos-sync.sh");
|
|
58408
59083
|
writeFileSync10(hookPath, hookScript);
|
|
58409
59084
|
execSync3(`chmod +x "${hookPath}"`);
|
|
58410
59085
|
console.log(chalk8.green(`Hook script created: ${hookPath}`));
|
|
58411
|
-
const settingsPath =
|
|
59086
|
+
const settingsPath = join22(process.cwd(), ".claude", "settings.json");
|
|
58412
59087
|
const settings = readJsonFile2(settingsPath);
|
|
58413
59088
|
if (!settings["hooks"]) {
|
|
58414
59089
|
settings["hooks"] = {};
|
|
@@ -58446,7 +59121,11 @@ exit 0
|
|
|
58446
59121
|
unregisterMcp(opts.unregister, opts.global);
|
|
58447
59122
|
return;
|
|
58448
59123
|
}
|
|
58449
|
-
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);
|
|
58450
59129
|
});
|
|
58451
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) => {
|
|
58452
59131
|
const globalOpts = program2.opts();
|
|
@@ -59277,7 +59956,7 @@ Artifacts:`));
|
|
|
59277
59956
|
const gitDir = execSync3("git rev-parse --git-dir", { encoding: "utf-8" }).trim();
|
|
59278
59957
|
const hookPath = `${gitDir}/hooks/post-commit`;
|
|
59279
59958
|
const marker = "# todos-auto-link";
|
|
59280
|
-
if (
|
|
59959
|
+
if (existsSync22(hookPath)) {
|
|
59281
59960
|
const existing = readFileSync17(hookPath, "utf-8");
|
|
59282
59961
|
if (existing.includes(marker)) {
|
|
59283
59962
|
console.log(chalk8.yellow("Hook already installed."));
|
|
@@ -59305,7 +59984,7 @@ $(dirname "$0")/../../scripts/post-commit-hook.sh
|
|
|
59305
59984
|
const gitDir = execSync3("git rev-parse --git-dir", { encoding: "utf-8" }).trim();
|
|
59306
59985
|
const hookPath = `${gitDir}/hooks/post-commit`;
|
|
59307
59986
|
const marker = "# todos-auto-link";
|
|
59308
|
-
if (!
|
|
59987
|
+
if (!existsSync22(hookPath)) {
|
|
59309
59988
|
console.log(chalk8.dim("No post-commit hook found."));
|
|
59310
59989
|
return;
|
|
59311
59990
|
}
|
|
@@ -59491,7 +60170,7 @@ import chalk10 from "chalk";
|
|
|
59491
60170
|
import { execSync as execSync4 } from "child_process";
|
|
59492
60171
|
import { readFileSync as readFileSync18, unlinkSync as unlinkSync2, writeFileSync as writeFileSync11 } from "fs";
|
|
59493
60172
|
import { tmpdir as tmpdir4 } from "os";
|
|
59494
|
-
import { join as
|
|
60173
|
+
import { join as join23 } from "path";
|
|
59495
60174
|
function getOrCreateLocalMachineName() {
|
|
59496
60175
|
return process.env["TODOS_MACHINE_NAME"] || __require("os").hostname() || "unknown";
|
|
59497
60176
|
}
|
|
@@ -59529,7 +60208,7 @@ function remoteTempPath(sshAddress) {
|
|
|
59529
60208
|
}
|
|
59530
60209
|
function readRemoteBridgeBundle(sshAddress) {
|
|
59531
60210
|
const remotePath = remoteTempPath(sshAddress);
|
|
59532
|
-
const localPath =
|
|
60211
|
+
const localPath = join23(tmpdir4(), `todos-bridge-pull-${uuid()}.json`);
|
|
59533
60212
|
try {
|
|
59534
60213
|
runSsh(sshAddress, `todos export --format bridge --allow-plaintext-sensitive --output ${shellQuote(remotePath)}`, 120000);
|
|
59535
60214
|
scpFromRemote(sshAddress, remotePath, localPath);
|
|
@@ -59544,7 +60223,7 @@ function readRemoteBridgeBundle(sshAddress) {
|
|
|
59544
60223
|
}
|
|
59545
60224
|
}
|
|
59546
60225
|
function writeLocalBridgeBundle() {
|
|
59547
|
-
const localPath =
|
|
60226
|
+
const localPath = join23(tmpdir4(), `todos-bridge-push-${uuid()}.json`);
|
|
59548
60227
|
writeFileSync11(localPath, JSON.stringify(createLocalBridgeBundle(), null, 2));
|
|
59549
60228
|
return localPath;
|
|
59550
60229
|
}
|
|
@@ -60608,7 +61287,7 @@ __export(exports_onboarding_commands, {
|
|
|
60608
61287
|
registerOnboardingCommands: () => registerOnboardingCommands
|
|
60609
61288
|
});
|
|
60610
61289
|
import chalk17 from "chalk";
|
|
60611
|
-
import { resolve as
|
|
61290
|
+
import { resolve as resolve22 } from "path";
|
|
60612
61291
|
function registerOnboardingCommands(program2) {
|
|
60613
61292
|
program2.command("onboarding").alias("demo-fixtures").description("List, show, write, or import bundled local onboarding fixtures").option("--show <name>", "Show one fixture bridge bundle as JSON").option("--write <dir>", "Write all bundled fixture bridge bundles to a directory").option("--import <name>", "Dry-run or apply an onboarding fixture import").option("--apply", "Apply an onboarding fixture import. Defaults to dry-run.").option("--resolve-conflicts", "Safely merge existing local tasks while preserving divergent fields").action(async (opts) => {
|
|
60614
61293
|
const globalOpts = program2.opts();
|
|
@@ -60624,7 +61303,7 @@ function registerOnboardingCommands(program2) {
|
|
|
60624
61303
|
return;
|
|
60625
61304
|
}
|
|
60626
61305
|
if (opts.write) {
|
|
60627
|
-
const result = writeOnboardingFixtureFiles2(
|
|
61306
|
+
const result = writeOnboardingFixtureFiles2(resolve22(opts.write));
|
|
60628
61307
|
if (globalOpts.json) {
|
|
60629
61308
|
output(result, true);
|
|
60630
61309
|
return;
|
|
@@ -60838,7 +61517,6 @@ var init_cli_mcp_parity = __esm(() => {
|
|
|
60838
61517
|
"bulk_create_tasks",
|
|
60839
61518
|
"get_next_task",
|
|
60840
61519
|
"claim_next_task",
|
|
60841
|
-
"get_active_work",
|
|
60842
61520
|
"get_stale_tasks",
|
|
60843
61521
|
"get_my_tasks",
|
|
60844
61522
|
"get_blocked_tasks",
|
|
@@ -61870,11 +62548,8 @@ var init_cli_mcp_parity = __esm(() => {
|
|
|
61870
62548
|
"delete_search_view",
|
|
61871
62549
|
"get_status",
|
|
61872
62550
|
"standup",
|
|
61873
|
-
"get_task_stats",
|
|
61874
62551
|
"get_context",
|
|
61875
|
-
"task_context"
|
|
61876
|
-
"get_task_graph",
|
|
61877
|
-
"get_recent_activity"
|
|
62552
|
+
"task_context"
|
|
61878
62553
|
],
|
|
61879
62554
|
jsonContracts: ["task", "saved_search_view", "saved_search_run_result", "status_summary", "audit_history", "structured_error", "api_error"],
|
|
61880
62555
|
errorContracts: ["structured_error", "api_error"],
|
|
@@ -64073,7 +64748,7 @@ __export(exports_sdk_integration_fixtures, {
|
|
|
64073
64748
|
TODOS_SDK_INTEGRATION_FIXTURE_GENERATED_AT: () => TODOS_SDK_INTEGRATION_FIXTURE_GENERATED_AT
|
|
64074
64749
|
});
|
|
64075
64750
|
import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync12 } from "fs";
|
|
64076
|
-
import { join as
|
|
64751
|
+
import { join as join24 } from "path";
|
|
64077
64752
|
function source5(version) {
|
|
64078
64753
|
return {
|
|
64079
64754
|
packageName: "@hasna/todos",
|
|
@@ -64180,7 +64855,7 @@ function writeSdkIntegrationFixtures(directory, options = {}) {
|
|
|
64180
64855
|
];
|
|
64181
64856
|
const written = [];
|
|
64182
64857
|
for (const [name, payload] of files) {
|
|
64183
|
-
const file =
|
|
64858
|
+
const file = join24(directory, name);
|
|
64184
64859
|
writeFileSync12(file, `${JSON.stringify(payload, null, 2)}
|
|
64185
64860
|
`, "utf-8");
|
|
64186
64861
|
written.push(file);
|
|
@@ -64204,7 +64879,7 @@ __export(exports_sdk_fixture_commands, {
|
|
|
64204
64879
|
registerSdkFixtureCommands: () => registerSdkFixtureCommands
|
|
64205
64880
|
});
|
|
64206
64881
|
import chalk19 from "chalk";
|
|
64207
|
-
import { resolve as
|
|
64882
|
+
import { resolve as resolve23 } from "path";
|
|
64208
64883
|
function registerSdkFixtureCommands(program2) {
|
|
64209
64884
|
program2.command("sdk-fixtures").description("List, show, or write local SDK integration fixtures").option("--show", "Print the full fixture pack JSON").option("--write <dir>", "Write fixture pack, bridge fixture, contract snapshots, and example index to a directory").action(async (opts) => {
|
|
64210
64885
|
const globalOpts = program2.opts();
|
|
@@ -64215,7 +64890,7 @@ function registerSdkFixtureCommands(program2) {
|
|
|
64215
64890
|
writeSdkIntegrationFixtures: writeSdkIntegrationFixtures2
|
|
64216
64891
|
} = await Promise.resolve().then(() => (init_sdk_integration_fixtures(), exports_sdk_integration_fixtures));
|
|
64217
64892
|
if (opts.write) {
|
|
64218
|
-
const result = writeSdkIntegrationFixtures2(
|
|
64893
|
+
const result = writeSdkIntegrationFixtures2(resolve23(opts.write));
|
|
64219
64894
|
if (globalOpts.json) {
|
|
64220
64895
|
console.log(JSON.stringify(result));
|
|
64221
64896
|
return;
|
|
@@ -65037,7 +65712,7 @@ __export(exports_local_backup_commands, {
|
|
|
65037
65712
|
registerLocalBackupCommands: () => registerLocalBackupCommands
|
|
65038
65713
|
});
|
|
65039
65714
|
import chalk26 from "chalk";
|
|
65040
|
-
import { resolve as
|
|
65715
|
+
import { resolve as resolve24 } from "path";
|
|
65041
65716
|
function globalOptions6(program2) {
|
|
65042
65717
|
const command = program2;
|
|
65043
65718
|
return command.optsWithGlobals?.() ?? program2.opts();
|
|
@@ -65059,10 +65734,10 @@ function registerLocalBackupCommands(program2) {
|
|
|
65059
65734
|
const projectId = opts.projectId ?? autoProject(globalOpts);
|
|
65060
65735
|
const backupBundle = createLocalBackup2({
|
|
65061
65736
|
project_id: projectId,
|
|
65062
|
-
output_path: opts.output ?
|
|
65737
|
+
output_path: opts.output ? resolve24(opts.output) : undefined
|
|
65063
65738
|
});
|
|
65064
65739
|
const result = {
|
|
65065
|
-
output_path: opts.output ?
|
|
65740
|
+
output_path: opts.output ? resolve24(opts.output) : null,
|
|
65066
65741
|
backup: backupBundle
|
|
65067
65742
|
};
|
|
65068
65743
|
if (opts.json || globalOpts.json) {
|
|
@@ -65387,7 +66062,7 @@ function upsertById(db, table, columns, row, updateClockColumn) {
|
|
|
65387
66062
|
const placeholders2 = presentColumns.map(() => "?").join(", ");
|
|
65388
66063
|
const values = presentColumns.map((column) => valueForColumn(column, row[column]));
|
|
65389
66064
|
const updateColumns = presentColumns.filter((column) => column !== "id");
|
|
65390
|
-
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(", ");
|
|
65391
66066
|
const clockGuard = updateClockColumn && presentColumns.includes(updateClockColumn) ? ` WHERE ${table}.${updateClockColumn} IS NULL OR ${table}.${updateClockColumn} <= excluded.${updateClockColumn}` : "";
|
|
65392
66067
|
const sql = updateSet ? `INSERT INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders2})
|
|
65393
66068
|
ON CONFLICT(id) DO UPDATE SET ${updateSet}${clockGuard}` : `INSERT OR IGNORE INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders2})`;
|
|
@@ -65831,7 +66506,9 @@ class PostgresTodosSyncStore {
|
|
|
65831
66506
|
deleted_at = EXCLUDED.deleted_at,
|
|
65832
66507
|
source_machine_id = EXCLUDED.source_machine_id,
|
|
65833
66508
|
version = EXCLUDED.version
|
|
65834
|
-
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))`, [
|
|
65835
66512
|
this.service,
|
|
65836
66513
|
entry.type,
|
|
65837
66514
|
entry.id,
|
|
@@ -66184,7 +66861,7 @@ class PostgresJsonRecordStore {
|
|
|
66184
66861
|
async upsert(type, value, context = {}) {
|
|
66185
66862
|
await this.ensureSchema();
|
|
66186
66863
|
const updatedAt = stringValue2(value.updated_at) ?? stringValue2(value.created_at) ?? new Date().toISOString();
|
|
66187
|
-
await this.options.client.query(`INSERT INTO ${this.tableName} (
|
|
66864
|
+
const result = await this.options.client.query(`INSERT INTO ${this.tableName} (
|
|
66188
66865
|
service, object_type, object_id, payload, updated_at,
|
|
66189
66866
|
deleted_at, source_machine_id, version
|
|
66190
66867
|
) VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, NULL, $6, $7)
|
|
@@ -66194,7 +66871,11 @@ class PostgresJsonRecordStore {
|
|
|
66194
66871
|
deleted_at = NULL,
|
|
66195
66872
|
source_machine_id = EXCLUDED.source_machine_id,
|
|
66196
66873
|
version = EXCLUDED.version
|
|
66197
|
-
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`, [
|
|
66198
66879
|
this.service,
|
|
66199
66880
|
type,
|
|
66200
66881
|
value.id,
|
|
@@ -66203,8 +66884,27 @@ class PostgresJsonRecordStore {
|
|
|
66203
66884
|
context.requestId ?? this.sourceMachineId ?? null,
|
|
66204
66885
|
numberValue3(value.version)
|
|
66205
66886
|
]);
|
|
66887
|
+
if (result.rows.length === 0) {
|
|
66888
|
+
const current = await this.get(type, value.id);
|
|
66889
|
+
if (current)
|
|
66890
|
+
return current;
|
|
66891
|
+
}
|
|
66206
66892
|
return value;
|
|
66207
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
|
+
}
|
|
66208
66908
|
async delete(type, id, context = {}) {
|
|
66209
66909
|
await this.ensureSchema();
|
|
66210
66910
|
const existing = await this.get(type, id);
|
|
@@ -66360,6 +67060,9 @@ async function updateTask2(id, input, store) {
|
|
|
66360
67060
|
}
|
|
66361
67061
|
async function startTask2(id, agentId, store) {
|
|
66362
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
|
+
}
|
|
66363
67066
|
return patchTask(task2, {
|
|
66364
67067
|
status: "in_progress",
|
|
66365
67068
|
assigned_to: task2.assigned_to ?? agentId,
|
|
@@ -66429,8 +67132,20 @@ async function getNextTask2(filters, store) {
|
|
|
66429
67132
|
return (await listTasks3({ ...filters, status: "pending", limit: 1 }, store))[0] ?? null;
|
|
66430
67133
|
}
|
|
66431
67134
|
async function claimNextTask2(agentId, filters, store) {
|
|
66432
|
-
const
|
|
66433
|
-
|
|
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;
|
|
66434
67149
|
}
|
|
66435
67150
|
async function getActiveWork2(filters, store) {
|
|
66436
67151
|
const tasks = await listTasks3({ ...filters, status: "in_progress" }, store);
|
|
@@ -66703,12 +67418,9 @@ async function nextTaskShortId2(projectId, store, context) {
|
|
|
66703
67418
|
const project = await store.get("projects", projectId);
|
|
66704
67419
|
if (!project?.task_prefix)
|
|
66705
67420
|
return null;
|
|
66706
|
-
const counter =
|
|
66707
|
-
|
|
66708
|
-
|
|
66709
|
-
task_counter: counter,
|
|
66710
|
-
updated_at: new Date().toISOString()
|
|
66711
|
-
}, context);
|
|
67421
|
+
const counter = await store.incrementProjectTaskCounter(projectId, context);
|
|
67422
|
+
if (counter === null)
|
|
67423
|
+
return null;
|
|
66712
67424
|
return `${project.task_prefix}-${String(counter).padStart(5, "0")}`;
|
|
66713
67425
|
}
|
|
66714
67426
|
async function generateProjectPrefix(name, store) {
|
|
@@ -66866,7 +67578,7 @@ var init_factory = __esm(() => {
|
|
|
66866
67578
|
});
|
|
66867
67579
|
|
|
66868
67580
|
// src/storage/s3-artifacts.ts
|
|
66869
|
-
import { createHash as
|
|
67581
|
+
import { createHash as createHash15, createHmac as createHmac2 } from "crypto";
|
|
66870
67582
|
function createTodosS3ArtifactStore(options) {
|
|
66871
67583
|
const requestFetch = options.fetch ?? fetch;
|
|
66872
67584
|
const now4 = options.now ?? (() => new Date);
|
|
@@ -67038,7 +67750,7 @@ function toAmzDate(date) {
|
|
|
67038
67750
|
return date.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
67039
67751
|
}
|
|
67040
67752
|
function sha256Hex(value) {
|
|
67041
|
-
return
|
|
67753
|
+
return createHash15("sha256").update(value).digest("hex");
|
|
67042
67754
|
}
|
|
67043
67755
|
function hmac(key, value) {
|
|
67044
67756
|
return createHmac2("sha256", key).update(value).digest();
|