@hasna/todos 0.11.57 → 0.11.59
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 +38 -0
- package/dist/cli/commands/mcp-hooks-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/index.js +1622 -285
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +703 -21
- package/dist/db/findings.d.ts +108 -0
- package/dist/db/findings.d.ts.map +1 -0
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/task-crud.d.ts +3 -1
- package/dist/db/task-crud.d.ts.map +1 -1
- package/dist/db/task-runs.d.ts +56 -0
- package/dist/db/task-runs.d.ts.map +1 -1
- package/dist/db/tasks.d.ts +2 -2
- package/dist/db/tasks.d.ts.map +1 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1071 -103
- package/dist/json-contracts.d.ts.map +1 -1
- package/dist/lib/access-profiles.d.ts.map +1 -1
- package/dist/lib/event-hooks.d.ts +1 -1
- package/dist/lib/event-hooks.d.ts.map +1 -1
- package/dist/lib/shared-events.d.ts +1 -1
- package/dist/lib/shared-events.d.ts.map +1 -1
- package/dist/mcp/index.js +1186 -115
- package/dist/mcp/token-utils.d.ts.map +1 -1
- package/dist/mcp/tools/task-crud.d.ts.map +1 -1
- package/dist/mcp/tools/task-resources.d.ts.map +1 -1
- package/dist/mcp.js +12 -1
- package/dist/registry.js +703 -21
- package/dist/release-provenance.json +3 -3
- package/dist/server/index.js +1186 -115
- package/dist/server/routes.d.ts +1 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/serve.d.ts +2 -0
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/storage.js +574 -97
- package/dist/types/index.d.ts +11 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/storage.js
CHANGED
|
@@ -1269,6 +1269,49 @@ var init_migrations = __esm(() => {
|
|
|
1269
1269
|
CREATE INDEX IF NOT EXISTS idx_local_retrospectives_plan ON local_retrospectives(plan_id);
|
|
1270
1270
|
CREATE INDEX IF NOT EXISTS idx_local_retrospectives_agent ON local_retrospectives(agent_id);
|
|
1271
1271
|
INSERT OR IGNORE INTO _migrations (id) VALUES (61);
|
|
1272
|
+
`,
|
|
1273
|
+
`
|
|
1274
|
+
CREATE TABLE IF NOT EXISTS task_run_transactions (
|
|
1275
|
+
id TEXT PRIMARY KEY,
|
|
1276
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1277
|
+
run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
|
|
1278
|
+
key TEXT NOT NULL,
|
|
1279
|
+
loop_id TEXT,
|
|
1280
|
+
loop_run_id TEXT,
|
|
1281
|
+
metadata TEXT DEFAULT '{}',
|
|
1282
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1283
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1284
|
+
UNIQUE(task_id, key)
|
|
1285
|
+
);
|
|
1286
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_transactions_task_key ON task_run_transactions(task_id, key);
|
|
1287
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_transactions_key ON task_run_transactions(key);
|
|
1288
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_transactions_run ON task_run_transactions(run_id);
|
|
1289
|
+
|
|
1290
|
+
CREATE TABLE IF NOT EXISTS task_findings (
|
|
1291
|
+
id TEXT PRIMARY KEY,
|
|
1292
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1293
|
+
run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
|
|
1294
|
+
fingerprint TEXT NOT NULL,
|
|
1295
|
+
title TEXT NOT NULL,
|
|
1296
|
+
severity TEXT NOT NULL DEFAULT 'medium' CHECK(severity IN ('low', 'medium', 'high', 'critical')),
|
|
1297
|
+
status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open', 'resolved', 'ignored')),
|
|
1298
|
+
source TEXT,
|
|
1299
|
+
summary TEXT,
|
|
1300
|
+
artifact_path TEXT,
|
|
1301
|
+
metadata TEXT DEFAULT '{}',
|
|
1302
|
+
first_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1303
|
+
last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1304
|
+
resolved_at TEXT,
|
|
1305
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1306
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1307
|
+
UNIQUE(task_id, fingerprint)
|
|
1308
|
+
);
|
|
1309
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_task ON task_findings(task_id);
|
|
1310
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_run ON task_findings(run_id);
|
|
1311
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_status ON task_findings(status);
|
|
1312
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_source ON task_findings(source);
|
|
1313
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_fingerprint ON task_findings(fingerprint);
|
|
1314
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (62);
|
|
1272
1315
|
`
|
|
1273
1316
|
];
|
|
1274
1317
|
});
|
|
@@ -1706,6 +1749,47 @@ function ensureSchema(db) {
|
|
|
1706
1749
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_run ON task_run_artifacts(run_id)");
|
|
1707
1750
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_task ON task_run_artifacts(task_id)");
|
|
1708
1751
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_path ON task_run_artifacts(path)");
|
|
1752
|
+
ensureTable("task_run_transactions", `
|
|
1753
|
+
CREATE TABLE task_run_transactions (
|
|
1754
|
+
id TEXT PRIMARY KEY,
|
|
1755
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1756
|
+
run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
|
|
1757
|
+
key TEXT NOT NULL,
|
|
1758
|
+
loop_id TEXT,
|
|
1759
|
+
loop_run_id TEXT,
|
|
1760
|
+
metadata TEXT DEFAULT '{}',
|
|
1761
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1762
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1763
|
+
UNIQUE(task_id, key)
|
|
1764
|
+
)`);
|
|
1765
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_transactions_task_key ON task_run_transactions(task_id, key)");
|
|
1766
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_transactions_key ON task_run_transactions(key)");
|
|
1767
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_transactions_run ON task_run_transactions(run_id)");
|
|
1768
|
+
ensureTable("task_findings", `
|
|
1769
|
+
CREATE TABLE task_findings (
|
|
1770
|
+
id TEXT PRIMARY KEY,
|
|
1771
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1772
|
+
run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
|
|
1773
|
+
fingerprint TEXT NOT NULL,
|
|
1774
|
+
title TEXT NOT NULL,
|
|
1775
|
+
severity TEXT NOT NULL DEFAULT 'medium' CHECK(severity IN ('low', 'medium', 'high', 'critical')),
|
|
1776
|
+
status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open', 'resolved', 'ignored')),
|
|
1777
|
+
source TEXT,
|
|
1778
|
+
summary TEXT,
|
|
1779
|
+
artifact_path TEXT,
|
|
1780
|
+
metadata TEXT DEFAULT '{}',
|
|
1781
|
+
first_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1782
|
+
last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1783
|
+
resolved_at TEXT,
|
|
1784
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1785
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1786
|
+
UNIQUE(task_id, fingerprint)
|
|
1787
|
+
)`);
|
|
1788
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_task ON task_findings(task_id)");
|
|
1789
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_run ON task_findings(run_id)");
|
|
1790
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_status ON task_findings(status)");
|
|
1791
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_source ON task_findings(source)");
|
|
1792
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_fingerprint ON task_findings(fingerprint)");
|
|
1709
1793
|
ensureTable("inbox_items", `
|
|
1710
1794
|
CREATE TABLE inbox_items (
|
|
1711
1795
|
id TEXT PRIMARY KEY,
|
|
@@ -4180,6 +4264,7 @@ var LOCAL_EVENT_TYPES = [
|
|
|
4180
4264
|
"task.blocked",
|
|
4181
4265
|
"task.started",
|
|
4182
4266
|
"task.completed",
|
|
4267
|
+
"task.updated",
|
|
4183
4268
|
"task.due",
|
|
4184
4269
|
"task.due_soon",
|
|
4185
4270
|
"task.failed",
|
|
@@ -4418,7 +4503,7 @@ async function testLocalEventHook(name, input) {
|
|
|
4418
4503
|
return emitLocalEventHooks({ ...input, hooks: [hook] });
|
|
4419
4504
|
}
|
|
4420
4505
|
|
|
4421
|
-
// node_modules/.bun/@hasna+events@0.1.
|
|
4506
|
+
// node_modules/.bun/@hasna+events@0.1.9/node_modules/@hasna/events/dist/index.js
|
|
4422
4507
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
4423
4508
|
import { existsSync as existsSync5 } from "fs";
|
|
4424
4509
|
import { homedir } from "os";
|
|
@@ -4435,17 +4520,30 @@ function getPathValue(input, path) {
|
|
|
4435
4520
|
return;
|
|
4436
4521
|
}, input);
|
|
4437
4522
|
}
|
|
4438
|
-
function wildcardToRegExp(pattern) {
|
|
4439
|
-
|
|
4440
|
-
|
|
4523
|
+
function wildcardToRegExp(pattern, options = {}) {
|
|
4524
|
+
let body = "";
|
|
4525
|
+
for (let index = 0;index < pattern.length; index += 1) {
|
|
4526
|
+
const char = pattern[index];
|
|
4527
|
+
if (char === "*") {
|
|
4528
|
+
if (pattern[index + 1] === "*") {
|
|
4529
|
+
body += ".*";
|
|
4530
|
+
index += 1;
|
|
4531
|
+
} else {
|
|
4532
|
+
body += options.segmentSafe ? "[^/]*" : ".*";
|
|
4533
|
+
}
|
|
4534
|
+
} else {
|
|
4535
|
+
body += char.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
4536
|
+
}
|
|
4537
|
+
}
|
|
4538
|
+
return new RegExp(`^${body}$`);
|
|
4441
4539
|
}
|
|
4442
|
-
function matchString(value, matcher) {
|
|
4540
|
+
function matchString(value, matcher, options = {}) {
|
|
4443
4541
|
if (matcher === undefined)
|
|
4444
4542
|
return true;
|
|
4445
4543
|
if (value === undefined)
|
|
4446
4544
|
return false;
|
|
4447
4545
|
const matchers = Array.isArray(matcher) ? matcher : [matcher];
|
|
4448
|
-
return matchers.some((item) => wildcardToRegExp(item).test(value));
|
|
4546
|
+
return matchers.some((item) => wildcardToRegExp(item, options).test(value));
|
|
4449
4547
|
}
|
|
4450
4548
|
function matchRecord(input, matcher) {
|
|
4451
4549
|
if (!matcher)
|
|
@@ -4453,7 +4551,9 @@ function matchRecord(input, matcher) {
|
|
|
4453
4551
|
return Object.entries(matcher).every(([path, expected]) => {
|
|
4454
4552
|
const actual = getPathValue(input, path);
|
|
4455
4553
|
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
4456
|
-
return matchString(actual === undefined ? undefined : String(actual), expected
|
|
4554
|
+
return matchString(actual === undefined ? undefined : String(actual), expected, {
|
|
4555
|
+
segmentSafe: path.endsWith("_path") || path.endsWith(".path")
|
|
4556
|
+
});
|
|
4457
4557
|
}
|
|
4458
4558
|
return actual === expected;
|
|
4459
4559
|
});
|
|
@@ -4473,7 +4573,6 @@ var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
|
|
|
4473
4573
|
function getEventsDataDir(override) {
|
|
4474
4574
|
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join4(homedir(), ".hasna", "events");
|
|
4475
4575
|
}
|
|
4476
|
-
|
|
4477
4576
|
class JsonEventsStore {
|
|
4478
4577
|
dataDir;
|
|
4479
4578
|
channelsPath;
|
|
@@ -4810,7 +4909,7 @@ class EventsClient {
|
|
|
4810
4909
|
}
|
|
4811
4910
|
return deliveries;
|
|
4812
4911
|
}
|
|
4813
|
-
async
|
|
4912
|
+
async matchChannel(id, input = {}) {
|
|
4814
4913
|
const channel = await this.store.getChannel(id);
|
|
4815
4914
|
if (!channel)
|
|
4816
4915
|
throw new Error(`Channel not found: ${id}`);
|
|
@@ -4827,6 +4926,34 @@ class EventsClient {
|
|
|
4827
4926
|
time: input.time,
|
|
4828
4927
|
id: input.id
|
|
4829
4928
|
});
|
|
4929
|
+
const matched = channelMatchesEvent(channel, event);
|
|
4930
|
+
return {
|
|
4931
|
+
channelId: channel.id,
|
|
4932
|
+
matched,
|
|
4933
|
+
event,
|
|
4934
|
+
filters: channel.filters,
|
|
4935
|
+
reason: matched ? undefined : channel.enabled ? "event did not match channel filters" : "channel is disabled"
|
|
4936
|
+
};
|
|
4937
|
+
}
|
|
4938
|
+
async testChannel(id, input = {}, options = {}) {
|
|
4939
|
+
const channel = await this.store.getChannel(id);
|
|
4940
|
+
if (!channel)
|
|
4941
|
+
throw new Error(`Channel not found: ${id}`);
|
|
4942
|
+
const match = await this.matchChannel(id, input);
|
|
4943
|
+
const event = match.event;
|
|
4944
|
+
if (options.honorFilters && !match.matched) {
|
|
4945
|
+
const timestamp = new Date().toISOString();
|
|
4946
|
+
const result2 = createDeliveryResult(event, channel, [{
|
|
4947
|
+
attempt: 1,
|
|
4948
|
+
status: "skipped",
|
|
4949
|
+
startedAt: timestamp,
|
|
4950
|
+
completedAt: timestamp,
|
|
4951
|
+
error: match.reason
|
|
4952
|
+
}]);
|
|
4953
|
+
result2.metadata = { reason: "filter_mismatch" };
|
|
4954
|
+
await this.store.appendDelivery(result2);
|
|
4955
|
+
return result2;
|
|
4956
|
+
}
|
|
4830
4957
|
const eventForChannel = await this.applyRedaction(event, channel);
|
|
4831
4958
|
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
4832
4959
|
await this.store.appendDelivery(result);
|
|
@@ -4926,6 +5053,90 @@ function normalizeRetryPolicy(policy) {
|
|
|
4926
5053
|
};
|
|
4927
5054
|
}
|
|
4928
5055
|
|
|
5056
|
+
// src/lib/shared-events.ts
|
|
5057
|
+
init_database();
|
|
5058
|
+
|
|
5059
|
+
// src/db/task-lists.ts
|
|
5060
|
+
init_types();
|
|
5061
|
+
init_database();
|
|
5062
|
+
function rowToTaskList(row) {
|
|
5063
|
+
return {
|
|
5064
|
+
...row,
|
|
5065
|
+
metadata: JSON.parse(row.metadata || "{}")
|
|
5066
|
+
};
|
|
5067
|
+
}
|
|
5068
|
+
function createTaskList(input, db) {
|
|
5069
|
+
const d = db || getDatabase();
|
|
5070
|
+
const id = uuid();
|
|
5071
|
+
const timestamp = now();
|
|
5072
|
+
const slug = input.slug || slugify(input.name);
|
|
5073
|
+
if (!input.project_id) {
|
|
5074
|
+
const existing = d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ?").get(slug);
|
|
5075
|
+
if (existing) {
|
|
5076
|
+
throw new Error(`Standalone task list with slug "${slug}" already exists`);
|
|
5077
|
+
}
|
|
5078
|
+
}
|
|
5079
|
+
d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at)
|
|
5080
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp]);
|
|
5081
|
+
return getTaskList(id, d);
|
|
5082
|
+
}
|
|
5083
|
+
function getTaskList(id, db) {
|
|
5084
|
+
const d = db || getDatabase();
|
|
5085
|
+
const row = d.query("SELECT * FROM task_lists WHERE id = ?").get(id);
|
|
5086
|
+
return row ? rowToTaskList(row) : null;
|
|
5087
|
+
}
|
|
5088
|
+
function getTaskListBySlug(slug, projectId, db) {
|
|
5089
|
+
const d = db || getDatabase();
|
|
5090
|
+
let row;
|
|
5091
|
+
if (projectId) {
|
|
5092
|
+
row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id = ?").get(slug, projectId);
|
|
5093
|
+
} else {
|
|
5094
|
+
row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id IS NULL").get(slug);
|
|
5095
|
+
}
|
|
5096
|
+
return row ? rowToTaskList(row) : null;
|
|
5097
|
+
}
|
|
5098
|
+
function listTaskLists(projectId, db) {
|
|
5099
|
+
const d = db || getDatabase();
|
|
5100
|
+
if (projectId) {
|
|
5101
|
+
return d.query("SELECT * FROM task_lists WHERE project_id = ? ORDER BY name").all(projectId).map(rowToTaskList);
|
|
5102
|
+
}
|
|
5103
|
+
return d.query("SELECT * FROM task_lists ORDER BY name").all().map(rowToTaskList);
|
|
5104
|
+
}
|
|
5105
|
+
function updateTaskList(id, input, db) {
|
|
5106
|
+
const d = db || getDatabase();
|
|
5107
|
+
const existing = getTaskList(id, d);
|
|
5108
|
+
if (!existing)
|
|
5109
|
+
throw new TaskListNotFoundError(id);
|
|
5110
|
+
const sets = ["updated_at = ?"];
|
|
5111
|
+
const params = [now()];
|
|
5112
|
+
if (input.name !== undefined) {
|
|
5113
|
+
sets.push("name = ?");
|
|
5114
|
+
params.push(input.name);
|
|
5115
|
+
}
|
|
5116
|
+
if (input.description !== undefined) {
|
|
5117
|
+
sets.push("description = ?");
|
|
5118
|
+
params.push(input.description);
|
|
5119
|
+
}
|
|
5120
|
+
if (input.metadata !== undefined) {
|
|
5121
|
+
sets.push("metadata = ?");
|
|
5122
|
+
params.push(JSON.stringify(input.metadata));
|
|
5123
|
+
}
|
|
5124
|
+
params.push(id);
|
|
5125
|
+
d.run(`UPDATE task_lists SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
5126
|
+
return getTaskList(id, d);
|
|
5127
|
+
}
|
|
5128
|
+
function deleteTaskList(id, db) {
|
|
5129
|
+
const d = db || getDatabase();
|
|
5130
|
+
return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
|
|
5131
|
+
}
|
|
5132
|
+
function ensureTaskList(name, slug, projectId, db) {
|
|
5133
|
+
const d = db || getDatabase();
|
|
5134
|
+
const existing = getTaskListBySlug(slug, projectId, d);
|
|
5135
|
+
if (existing)
|
|
5136
|
+
return existing;
|
|
5137
|
+
return createTaskList({ name, slug, project_id: projectId }, d);
|
|
5138
|
+
}
|
|
5139
|
+
|
|
4929
5140
|
// src/lib/shared-events.ts
|
|
4930
5141
|
var SOURCE = "todos";
|
|
4931
5142
|
function taskEventData(task, extra = {}) {
|
|
@@ -4956,6 +5167,69 @@ function taskEventData(task, extra = {}) {
|
|
|
4956
5167
|
...extra
|
|
4957
5168
|
};
|
|
4958
5169
|
}
|
|
5170
|
+
function taskEventMetadata(task) {
|
|
5171
|
+
const metadata = {
|
|
5172
|
+
package: "@hasna/todos",
|
|
5173
|
+
todos_event_schema_version: 1,
|
|
5174
|
+
task_id: task.id,
|
|
5175
|
+
task_short_id: task.short_id,
|
|
5176
|
+
project_id: task.project_id,
|
|
5177
|
+
task_list_id: task.task_list_id,
|
|
5178
|
+
working_dir: task.working_dir
|
|
5179
|
+
};
|
|
5180
|
+
try {
|
|
5181
|
+
const project = task.project_id ? getProject(task.project_id) : null;
|
|
5182
|
+
const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
|
|
5183
|
+
if (project) {
|
|
5184
|
+
metadata.project_id = project.id;
|
|
5185
|
+
metadata.project_name = project.name;
|
|
5186
|
+
metadata.project_path = projectPath;
|
|
5187
|
+
metadata.project_canonical_path = project.path;
|
|
5188
|
+
metadata.project_default_task_list_slug = project.task_list_id;
|
|
5189
|
+
metadata.root_project_id = inferRootProjectId(project);
|
|
5190
|
+
} else if (projectPath) {
|
|
5191
|
+
metadata.project_path = projectPath;
|
|
5192
|
+
metadata.project_canonical_path = projectPath;
|
|
5193
|
+
}
|
|
5194
|
+
if (projectPath) {
|
|
5195
|
+
metadata.project_kind = classifyProjectKind(projectPath);
|
|
5196
|
+
metadata.project_is_worktree = isWorktreePath(projectPath);
|
|
5197
|
+
if (typeof task.metadata.route_enabled === "boolean") {
|
|
5198
|
+
metadata.route_enabled = task.metadata.route_enabled;
|
|
5199
|
+
}
|
|
5200
|
+
metadata.working_dir = task.working_dir ?? projectPath;
|
|
5201
|
+
}
|
|
5202
|
+
const taskList = task.task_list_id ? getTaskList(task.task_list_id) ?? (project ? getTaskListBySlug(task.task_list_id, project.id) : null) : project?.task_list_id ? getTaskListBySlug(project.task_list_id, project.id) : null;
|
|
5203
|
+
if (taskList) {
|
|
5204
|
+
metadata.task_list_id = taskList.id;
|
|
5205
|
+
metadata.task_list_slug = taskList.slug;
|
|
5206
|
+
metadata.task_list_name = taskList.name;
|
|
5207
|
+
metadata.task_list_project_id = taskList.project_id;
|
|
5208
|
+
metadata.task_list_is_project_default = Boolean(project?.task_list_id && taskList.slug === project.task_list_id);
|
|
5209
|
+
}
|
|
5210
|
+
} catch {}
|
|
5211
|
+
return metadata;
|
|
5212
|
+
}
|
|
5213
|
+
function classifyProjectKind(path) {
|
|
5214
|
+
return path.includes("/hasna/opensource/") ? "open-source" : "unknown";
|
|
5215
|
+
}
|
|
5216
|
+
function isWorktreePath(path) {
|
|
5217
|
+
return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
|
|
5218
|
+
}
|
|
5219
|
+
function inferRootProjectId(project) {
|
|
5220
|
+
return isWorktreePath(project.path) ? null : project.id;
|
|
5221
|
+
}
|
|
5222
|
+
function readMachineLocalPath(project) {
|
|
5223
|
+
const machineId = process.env["TODOS_MACHINE_ID"];
|
|
5224
|
+
if (!machineId)
|
|
5225
|
+
return null;
|
|
5226
|
+
try {
|
|
5227
|
+
const row = getDatabase().query("SELECT path FROM project_machine_paths WHERE project_id = ? AND machine_id = ?").get(project.id, machineId);
|
|
5228
|
+
return row?.path ?? null;
|
|
5229
|
+
} catch {
|
|
5230
|
+
return null;
|
|
5231
|
+
}
|
|
5232
|
+
}
|
|
4959
5233
|
async function emitSharedTaskEvent(input) {
|
|
4960
5234
|
const data = taskEventData(input.task, input.data);
|
|
4961
5235
|
await new EventsClient().emit({
|
|
@@ -4966,12 +5240,7 @@ async function emitSharedTaskEvent(input) {
|
|
|
4966
5240
|
message: input.message ?? `${input.type}: ${input.task.title}`,
|
|
4967
5241
|
data,
|
|
4968
5242
|
dedupeKey: input.dedupeKey ?? `${input.type}:${input.task.id}:${input.task.version}`,
|
|
4969
|
-
metadata:
|
|
4970
|
-
package: "@hasna/todos",
|
|
4971
|
-
task_id: input.task.id,
|
|
4972
|
-
project_id: input.task.project_id,
|
|
4973
|
-
task_list_id: input.task.task_list_id
|
|
4974
|
-
}
|
|
5243
|
+
metadata: taskEventMetadata(input.task)
|
|
4975
5244
|
}, { deliver: true, dedupe: true });
|
|
4976
5245
|
}
|
|
4977
5246
|
function emitSharedTaskEventQuiet(input) {
|
|
@@ -5334,6 +5603,17 @@ function replaceTaskTags(taskId, tags, db) {
|
|
|
5334
5603
|
db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
|
|
5335
5604
|
insertTaskTags(taskId, tags, db);
|
|
5336
5605
|
}
|
|
5606
|
+
function addMetadataConditions(metadata, conditions, params) {
|
|
5607
|
+
if (!metadata)
|
|
5608
|
+
return;
|
|
5609
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
5610
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(key)) {
|
|
5611
|
+
throw new Error(`Invalid metadata filter key: ${key}`);
|
|
5612
|
+
}
|
|
5613
|
+
conditions.push(`json_extract(metadata, '$."${key}"') = ?`);
|
|
5614
|
+
params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
|
|
5615
|
+
}
|
|
5616
|
+
}
|
|
5337
5617
|
function createTask(input, db) {
|
|
5338
5618
|
const d = db || getDatabase();
|
|
5339
5619
|
const timestamp = now();
|
|
@@ -5516,6 +5796,7 @@ function listTasks(filter = {}, db) {
|
|
|
5516
5796
|
params.push(filter.task_type);
|
|
5517
5797
|
}
|
|
5518
5798
|
}
|
|
5799
|
+
addMetadataConditions(filter.metadata, conditions, params);
|
|
5519
5800
|
const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
|
|
5520
5801
|
if (filter.cursor) {
|
|
5521
5802
|
try {
|
|
@@ -5540,6 +5821,54 @@ function listTasks(filter = {}, db) {
|
|
|
5540
5821
|
const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC${limitClause}`).all(...params);
|
|
5541
5822
|
return rows.map(rowToTask);
|
|
5542
5823
|
}
|
|
5824
|
+
function getTaskByFingerprint(fingerprint, db) {
|
|
5825
|
+
const tasks = listTasks({ metadata: { fingerprint }, limit: 1 }, db);
|
|
5826
|
+
return tasks[0] ?? null;
|
|
5827
|
+
}
|
|
5828
|
+
function mergeTaskMetadata(current, next, fingerprint) {
|
|
5829
|
+
return {
|
|
5830
|
+
...current,
|
|
5831
|
+
...next ?? {},
|
|
5832
|
+
fingerprint
|
|
5833
|
+
};
|
|
5834
|
+
}
|
|
5835
|
+
function upsertTaskByFingerprint(input, db) {
|
|
5836
|
+
const d = db || getDatabase();
|
|
5837
|
+
const fingerprint = input.fingerprint.trim();
|
|
5838
|
+
if (!fingerprint)
|
|
5839
|
+
throw new Error("fingerprint is required");
|
|
5840
|
+
const existing = getTaskByFingerprint(fingerprint, d);
|
|
5841
|
+
const metadata = mergeTaskMetadata(existing?.metadata ?? {}, input.metadata, fingerprint);
|
|
5842
|
+
if (!existing) {
|
|
5843
|
+
const task2 = createTask({ ...input, metadata }, d);
|
|
5844
|
+
return { task: task2, created: true };
|
|
5845
|
+
}
|
|
5846
|
+
const task = updateTask(existing.id, {
|
|
5847
|
+
version: existing.version,
|
|
5848
|
+
title: input.title,
|
|
5849
|
+
description: input.description,
|
|
5850
|
+
status: input.status,
|
|
5851
|
+
priority: input.priority,
|
|
5852
|
+
project_id: input.project_id,
|
|
5853
|
+
assigned_to: input.assigned_to,
|
|
5854
|
+
working_dir: input.working_dir,
|
|
5855
|
+
plan_id: input.plan_id,
|
|
5856
|
+
task_list_id: input.task_list_id,
|
|
5857
|
+
tags: input.tags,
|
|
5858
|
+
metadata,
|
|
5859
|
+
due_at: input.due_at,
|
|
5860
|
+
estimated_minutes: input.estimated_minutes,
|
|
5861
|
+
sla_minutes: input.sla_minutes,
|
|
5862
|
+
confidence: input.confidence,
|
|
5863
|
+
retry_count: input.retry_count,
|
|
5864
|
+
max_retries: input.max_retries,
|
|
5865
|
+
retry_after: input.retry_after,
|
|
5866
|
+
requires_approval: input.requires_approval,
|
|
5867
|
+
recurrence_rule: input.recurrence_rule,
|
|
5868
|
+
task_type: input.task_type
|
|
5869
|
+
}, d);
|
|
5870
|
+
return { task, created: false };
|
|
5871
|
+
}
|
|
5543
5872
|
function countTasks(filter = {}, db) {
|
|
5544
5873
|
const d = db || getDatabase();
|
|
5545
5874
|
const conditions = [];
|
|
@@ -5603,6 +5932,7 @@ function countTasks(filter = {}, db) {
|
|
|
5603
5932
|
conditions.push("task_list_id = ?");
|
|
5604
5933
|
params.push(filter.task_list_id);
|
|
5605
5934
|
}
|
|
5935
|
+
addMetadataConditions(filter.metadata, conditions, params);
|
|
5606
5936
|
if (!filter.include_archived) {
|
|
5607
5937
|
conditions.push("archived_at IS NULL");
|
|
5608
5938
|
}
|
|
@@ -5653,6 +5983,10 @@ function updateTask(id, input, db) {
|
|
|
5653
5983
|
sets.push("assigned_to = ?");
|
|
5654
5984
|
params.push(input.assigned_to);
|
|
5655
5985
|
}
|
|
5986
|
+
if (input.working_dir !== undefined) {
|
|
5987
|
+
sets.push("working_dir = ?");
|
|
5988
|
+
params.push(input.working_dir);
|
|
5989
|
+
}
|
|
5656
5990
|
if (input.tags !== undefined) {
|
|
5657
5991
|
sets.push("tags = ?");
|
|
5658
5992
|
params.push(JSON.stringify(input.tags));
|
|
@@ -5741,6 +6075,8 @@ function updateTask(id, input, db) {
|
|
|
5741
6075
|
logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
|
|
5742
6076
|
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
|
|
5743
6077
|
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
6078
|
+
if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
|
|
6079
|
+
logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
|
|
5744
6080
|
if (input.approved_by !== undefined)
|
|
5745
6081
|
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
5746
6082
|
const updatedTask = {
|
|
@@ -5776,6 +6112,10 @@ function updateTask(id, input, db) {
|
|
|
5776
6112
|
if (input.approved_by !== undefined) {
|
|
5777
6113
|
emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
|
|
5778
6114
|
}
|
|
6115
|
+
const updatePayload = taskEventData(updatedTask);
|
|
6116
|
+
dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
|
|
6117
|
+
emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload });
|
|
6118
|
+
emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask });
|
|
5779
6119
|
return updatedTask;
|
|
5780
6120
|
}
|
|
5781
6121
|
function deleteTask(id, db) {
|
|
@@ -8471,6 +8811,7 @@ function getTaskTraceability(taskId, db) {
|
|
|
8471
8811
|
|
|
8472
8812
|
// src/db/task-runs.ts
|
|
8473
8813
|
init_redaction();
|
|
8814
|
+
var LOOP_RUN_TRANSACTION_SCHEMA_VERSION = "todos.loop_run_transaction.v1";
|
|
8474
8815
|
function parseObject(value) {
|
|
8475
8816
|
if (!value)
|
|
8476
8817
|
return {};
|
|
@@ -8493,6 +8834,72 @@ function rowToArtifact(row) {
|
|
|
8493
8834
|
function getRunRow(runId, db) {
|
|
8494
8835
|
return db.query("SELECT * FROM task_runs WHERE id = ?").get(runId);
|
|
8495
8836
|
}
|
|
8837
|
+
function normalizeTransactionKey(input) {
|
|
8838
|
+
const key = (input.key || input.loop_run_id || input.loop_id || "").trim();
|
|
8839
|
+
if (!key)
|
|
8840
|
+
throw new Error("idempotent run transactions require --key, --loop-run-id, or --loop-id");
|
|
8841
|
+
return key.toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 240);
|
|
8842
|
+
}
|
|
8843
|
+
function loopTransactionMetadata(record) {
|
|
8844
|
+
const value = record.metadata["loop_transaction"];
|
|
8845
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
8846
|
+
}
|
|
8847
|
+
function runKey(record) {
|
|
8848
|
+
const tx = loopTransactionMetadata(record);
|
|
8849
|
+
const key = tx["idempotency_key"] ?? record.metadata["idempotency_key"];
|
|
8850
|
+
return typeof key === "string" ? key : null;
|
|
8851
|
+
}
|
|
8852
|
+
function loopId(record) {
|
|
8853
|
+
const tx = loopTransactionMetadata(record);
|
|
8854
|
+
const value = tx["loop_id"] ?? record.metadata["loop_id"];
|
|
8855
|
+
return typeof value === "string" ? value : null;
|
|
8856
|
+
}
|
|
8857
|
+
function loopRunId(record) {
|
|
8858
|
+
const tx = loopTransactionMetadata(record);
|
|
8859
|
+
const value = tx["loop_run_id"] ?? record.metadata["loop_run_id"];
|
|
8860
|
+
return typeof value === "string" ? value : null;
|
|
8861
|
+
}
|
|
8862
|
+
function getTaskRunTransactionByKey(key, taskId, db) {
|
|
8863
|
+
if (taskId) {
|
|
8864
|
+
return db.query("SELECT * FROM task_run_transactions WHERE task_id = ? AND key = ?").get(taskId, key);
|
|
8865
|
+
}
|
|
8866
|
+
const rows = db.query("SELECT * FROM task_run_transactions WHERE key = ? ORDER BY created_at DESC LIMIT 2").all(key);
|
|
8867
|
+
if (rows.length > 1)
|
|
8868
|
+
throw new Error(`Run transaction key is ambiguous across tasks: ${key}. Pass task_id.`);
|
|
8869
|
+
return rows[0] ?? null;
|
|
8870
|
+
}
|
|
8871
|
+
function summarizeTaskRun(run) {
|
|
8872
|
+
return {
|
|
8873
|
+
id: run.id,
|
|
8874
|
+
task_id: run.task_id,
|
|
8875
|
+
agent_id: run.agent_id,
|
|
8876
|
+
title: run.title,
|
|
8877
|
+
status: run.status,
|
|
8878
|
+
summary: run.summary,
|
|
8879
|
+
idempotency_key: runKey(run),
|
|
8880
|
+
loop_id: loopId(run),
|
|
8881
|
+
loop_run_id: loopRunId(run),
|
|
8882
|
+
metadata_keys: Object.keys(run.metadata).sort(),
|
|
8883
|
+
started_at: run.started_at,
|
|
8884
|
+
completed_at: run.completed_at,
|
|
8885
|
+
updated_at: run.updated_at
|
|
8886
|
+
};
|
|
8887
|
+
}
|
|
8888
|
+
function findTaskRunByTransactionKey(key, taskId, db) {
|
|
8889
|
+
const d = db || getDatabase();
|
|
8890
|
+
const normalized = normalizeTransactionKey({ key });
|
|
8891
|
+
const transaction = getTaskRunTransactionByKey(normalized, taskId, d);
|
|
8892
|
+
if (transaction?.run_id)
|
|
8893
|
+
return getTaskRun(transaction.run_id, d);
|
|
8894
|
+
return null;
|
|
8895
|
+
}
|
|
8896
|
+
function loopRunCommands(run, key) {
|
|
8897
|
+
return [
|
|
8898
|
+
run ? `todos runs show ${run.id.slice(0, 8)}` : "todos runs list",
|
|
8899
|
+
run ? `todos findings list --task ${run.task_id.slice(0, 8)} --json` : "todos findings list --json",
|
|
8900
|
+
`todos runs begin <task-id> --key ${key} --apply --json`
|
|
8901
|
+
];
|
|
8902
|
+
}
|
|
8496
8903
|
function resolveTaskRunId(idOrPrefix, db) {
|
|
8497
8904
|
const d = db || getDatabase();
|
|
8498
8905
|
const rows = d.query("SELECT id FROM task_runs WHERE id = ? OR id LIKE ? ORDER BY created_at DESC LIMIT 2").all(idOrPrefix, `${idOrPrefix}%`);
|
|
@@ -8511,7 +8918,7 @@ function startTaskRun(input, db) {
|
|
|
8511
8918
|
const d = db || getDatabase();
|
|
8512
8919
|
if (!getTask(input.task_id, d))
|
|
8513
8920
|
throw new TaskNotFoundError(input.task_id);
|
|
8514
|
-
const id = uuid();
|
|
8921
|
+
const id = input.id ?? uuid();
|
|
8515
8922
|
const timestamp = input.started_at || now();
|
|
8516
8923
|
if (input.claim && input.agent_id) {
|
|
8517
8924
|
startTask(input.task_id, input.agent_id, d);
|
|
@@ -8549,6 +8956,97 @@ function startTaskRun(input, db) {
|
|
|
8549
8956
|
emitLocalEventHooksQuiet({ type: "run.started", payload: { id: run.id, task_id: run.task_id, agent_id: run.agent_id, title: run.title } });
|
|
8550
8957
|
return run;
|
|
8551
8958
|
}
|
|
8959
|
+
function beginTaskRunTransaction(input, db) {
|
|
8960
|
+
const d = db || getDatabase();
|
|
8961
|
+
if (!getTask(input.task_id, d))
|
|
8962
|
+
throw new TaskNotFoundError(input.task_id);
|
|
8963
|
+
const timestamp = input.started_at || now();
|
|
8964
|
+
const key = normalizeTransactionKey(input);
|
|
8965
|
+
const existing = findTaskRunByTransactionKey(key, input.task_id, d);
|
|
8966
|
+
const dryRun = !input.apply;
|
|
8967
|
+
if (existing) {
|
|
8968
|
+
return {
|
|
8969
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
8970
|
+
local_only: true,
|
|
8971
|
+
dry_run: dryRun,
|
|
8972
|
+
processed_at: timestamp,
|
|
8973
|
+
action: "matched",
|
|
8974
|
+
key,
|
|
8975
|
+
run: summarizeTaskRun(existing),
|
|
8976
|
+
warnings: existing.status === "running" ? [] : [`matched ${existing.status} run`],
|
|
8977
|
+
commands: loopRunCommands(existing, key)
|
|
8978
|
+
};
|
|
8979
|
+
}
|
|
8980
|
+
if (dryRun) {
|
|
8981
|
+
return {
|
|
8982
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
8983
|
+
local_only: true,
|
|
8984
|
+
dry_run: true,
|
|
8985
|
+
processed_at: timestamp,
|
|
8986
|
+
action: "preview",
|
|
8987
|
+
key,
|
|
8988
|
+
run: null,
|
|
8989
|
+
warnings: [],
|
|
8990
|
+
commands: loopRunCommands(null, key)
|
|
8991
|
+
};
|
|
8992
|
+
}
|
|
8993
|
+
const metadata = redactValue({
|
|
8994
|
+
...input.metadata || {},
|
|
8995
|
+
loop_transaction: {
|
|
8996
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
8997
|
+
idempotency_key: key,
|
|
8998
|
+
loop_id: input.loop_id ?? null,
|
|
8999
|
+
loop_run_id: input.loop_run_id ?? null,
|
|
9000
|
+
first_seen_at: timestamp
|
|
9001
|
+
},
|
|
9002
|
+
idempotency_key: key
|
|
9003
|
+
});
|
|
9004
|
+
const created = d.transaction(() => {
|
|
9005
|
+
d.run(`INSERT OR IGNORE INTO task_run_transactions (
|
|
9006
|
+
id, task_id, run_id, key, loop_id, loop_run_id, metadata, created_at, updated_at
|
|
9007
|
+
) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?)`, [
|
|
9008
|
+
uuid(),
|
|
9009
|
+
input.task_id,
|
|
9010
|
+
key,
|
|
9011
|
+
input.loop_id ?? null,
|
|
9012
|
+
input.loop_run_id ?? null,
|
|
9013
|
+
JSON.stringify(metadata),
|
|
9014
|
+
timestamp,
|
|
9015
|
+
timestamp
|
|
9016
|
+
]);
|
|
9017
|
+
const transaction = getTaskRunTransactionByKey(key, input.task_id, d);
|
|
9018
|
+
if (!transaction)
|
|
9019
|
+
throw new Error(`Could not create run transaction for key: ${key}`);
|
|
9020
|
+
if (transaction.run_id) {
|
|
9021
|
+
const existingRun = getTaskRun(transaction.run_id, d);
|
|
9022
|
+
if (existingRun)
|
|
9023
|
+
return { run: existingRun, action: "matched" };
|
|
9024
|
+
}
|
|
9025
|
+
const run = startTaskRun({
|
|
9026
|
+
id: uuid(),
|
|
9027
|
+
task_id: input.task_id,
|
|
9028
|
+
agent_id: input.agent_id,
|
|
9029
|
+
title: input.title,
|
|
9030
|
+
summary: input.summary,
|
|
9031
|
+
metadata,
|
|
9032
|
+
claim: input.claim,
|
|
9033
|
+
started_at: timestamp
|
|
9034
|
+
}, d);
|
|
9035
|
+
d.run("UPDATE task_run_transactions SET run_id = ?, loop_id = COALESCE(?, loop_id), loop_run_id = COALESCE(?, loop_run_id), metadata = ?, updated_at = ? WHERE id = ?", [run.id, input.loop_id ?? null, input.loop_run_id ?? null, JSON.stringify(metadata), timestamp, transaction.id]);
|
|
9036
|
+
return { run, action: "created" };
|
|
9037
|
+
})();
|
|
9038
|
+
return {
|
|
9039
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
9040
|
+
local_only: true,
|
|
9041
|
+
dry_run: false,
|
|
9042
|
+
processed_at: timestamp,
|
|
9043
|
+
action: created.action,
|
|
9044
|
+
key,
|
|
9045
|
+
run: summarizeTaskRun(created.run),
|
|
9046
|
+
warnings: [],
|
|
9047
|
+
commands: loopRunCommands(created.run, key)
|
|
9048
|
+
};
|
|
9049
|
+
}
|
|
8552
9050
|
function addTaskRunEvent(input, db) {
|
|
8553
9051
|
const d = db || getDatabase();
|
|
8554
9052
|
const runId = resolveTaskRunId(input.run_id, d);
|
|
@@ -8728,6 +9226,66 @@ function finishTaskRun(input, db) {
|
|
|
8728
9226
|
});
|
|
8729
9227
|
return updated;
|
|
8730
9228
|
}
|
|
9229
|
+
function finishTaskRunTransaction(input, db) {
|
|
9230
|
+
const d = db || getDatabase();
|
|
9231
|
+
const timestamp = input.completed_at || now();
|
|
9232
|
+
const status = input.status || "completed";
|
|
9233
|
+
const key = input.key ? normalizeTransactionKey({ key: input.key }) : "";
|
|
9234
|
+
const run = input.run_id ? getTaskRun(resolveTaskRunId(input.run_id, d), d) : key ? findTaskRunByTransactionKey(key, input.task_id, d) : null;
|
|
9235
|
+
if (!run) {
|
|
9236
|
+
throw new Error(input.run_id ? `Run not found: ${input.run_id}` : "runs finish requires a run id or --key");
|
|
9237
|
+
}
|
|
9238
|
+
if (input.task_id && run.task_id !== input.task_id) {
|
|
9239
|
+
throw new Error(`Run ${run.id} belongs to task ${run.task_id}, not ${input.task_id}`);
|
|
9240
|
+
}
|
|
9241
|
+
const resolvedKey = key || runKey(run) || run.id;
|
|
9242
|
+
const dryRun = input.apply === false;
|
|
9243
|
+
if (run.status !== "running") {
|
|
9244
|
+
const conflict = run.status !== status;
|
|
9245
|
+
return {
|
|
9246
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
9247
|
+
local_only: true,
|
|
9248
|
+
dry_run: dryRun,
|
|
9249
|
+
processed_at: timestamp,
|
|
9250
|
+
action: conflict ? "conflict" : "matched",
|
|
9251
|
+
key: resolvedKey,
|
|
9252
|
+
run: summarizeTaskRun(run),
|
|
9253
|
+
warnings: conflict ? [`run is already ${run.status}; requested ${status}`] : [],
|
|
9254
|
+
commands: loopRunCommands(run, resolvedKey)
|
|
9255
|
+
};
|
|
9256
|
+
}
|
|
9257
|
+
if (dryRun) {
|
|
9258
|
+
return {
|
|
9259
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
9260
|
+
local_only: true,
|
|
9261
|
+
dry_run: true,
|
|
9262
|
+
processed_at: timestamp,
|
|
9263
|
+
action: "preview",
|
|
9264
|
+
key: resolvedKey,
|
|
9265
|
+
run: summarizeTaskRun({ ...run, status, summary: input.summary ?? run.summary, completed_at: timestamp, updated_at: timestamp }),
|
|
9266
|
+
warnings: [],
|
|
9267
|
+
commands: loopRunCommands(run, resolvedKey)
|
|
9268
|
+
};
|
|
9269
|
+
}
|
|
9270
|
+
const finished = finishTaskRun({
|
|
9271
|
+
run_id: run.id,
|
|
9272
|
+
status,
|
|
9273
|
+
summary: input.summary,
|
|
9274
|
+
agent_id: input.agent_id,
|
|
9275
|
+
completed_at: timestamp
|
|
9276
|
+
}, d);
|
|
9277
|
+
return {
|
|
9278
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
9279
|
+
local_only: true,
|
|
9280
|
+
dry_run: false,
|
|
9281
|
+
processed_at: timestamp,
|
|
9282
|
+
action: "finished",
|
|
9283
|
+
key: resolvedKey,
|
|
9284
|
+
run: summarizeTaskRun(finished),
|
|
9285
|
+
warnings: [],
|
|
9286
|
+
commands: loopRunCommands(finished, resolvedKey)
|
|
9287
|
+
};
|
|
9288
|
+
}
|
|
8731
9289
|
function listTaskRuns(taskId, db) {
|
|
8732
9290
|
const d = db || getDatabase();
|
|
8733
9291
|
const rows = taskId ? d.query("SELECT * FROM task_runs WHERE task_id = ? ORDER BY started_at DESC, created_at DESC").all(taskId) : d.query("SELECT * FROM task_runs ORDER BY started_at DESC, created_at DESC LIMIT 100").all();
|
|
@@ -9741,87 +10299,6 @@ function getCapableAgents(capabilities, opts, db) {
|
|
|
9741
10299
|
return opts?.limit ? scored.slice(0, opts.limit) : scored;
|
|
9742
10300
|
}
|
|
9743
10301
|
|
|
9744
|
-
// src/db/task-lists.ts
|
|
9745
|
-
init_types();
|
|
9746
|
-
init_database();
|
|
9747
|
-
function rowToTaskList(row) {
|
|
9748
|
-
return {
|
|
9749
|
-
...row,
|
|
9750
|
-
metadata: JSON.parse(row.metadata || "{}")
|
|
9751
|
-
};
|
|
9752
|
-
}
|
|
9753
|
-
function createTaskList(input, db) {
|
|
9754
|
-
const d = db || getDatabase();
|
|
9755
|
-
const id = uuid();
|
|
9756
|
-
const timestamp = now();
|
|
9757
|
-
const slug = input.slug || slugify(input.name);
|
|
9758
|
-
if (!input.project_id) {
|
|
9759
|
-
const existing = d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ?").get(slug);
|
|
9760
|
-
if (existing) {
|
|
9761
|
-
throw new Error(`Standalone task list with slug "${slug}" already exists`);
|
|
9762
|
-
}
|
|
9763
|
-
}
|
|
9764
|
-
d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at)
|
|
9765
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp]);
|
|
9766
|
-
return getTaskList(id, d);
|
|
9767
|
-
}
|
|
9768
|
-
function getTaskList(id, db) {
|
|
9769
|
-
const d = db || getDatabase();
|
|
9770
|
-
const row = d.query("SELECT * FROM task_lists WHERE id = ?").get(id);
|
|
9771
|
-
return row ? rowToTaskList(row) : null;
|
|
9772
|
-
}
|
|
9773
|
-
function getTaskListBySlug(slug, projectId, db) {
|
|
9774
|
-
const d = db || getDatabase();
|
|
9775
|
-
let row;
|
|
9776
|
-
if (projectId) {
|
|
9777
|
-
row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id = ?").get(slug, projectId);
|
|
9778
|
-
} else {
|
|
9779
|
-
row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id IS NULL").get(slug);
|
|
9780
|
-
}
|
|
9781
|
-
return row ? rowToTaskList(row) : null;
|
|
9782
|
-
}
|
|
9783
|
-
function listTaskLists(projectId, db) {
|
|
9784
|
-
const d = db || getDatabase();
|
|
9785
|
-
if (projectId) {
|
|
9786
|
-
return d.query("SELECT * FROM task_lists WHERE project_id = ? ORDER BY name").all(projectId).map(rowToTaskList);
|
|
9787
|
-
}
|
|
9788
|
-
return d.query("SELECT * FROM task_lists ORDER BY name").all().map(rowToTaskList);
|
|
9789
|
-
}
|
|
9790
|
-
function updateTaskList(id, input, db) {
|
|
9791
|
-
const d = db || getDatabase();
|
|
9792
|
-
const existing = getTaskList(id, d);
|
|
9793
|
-
if (!existing)
|
|
9794
|
-
throw new TaskListNotFoundError(id);
|
|
9795
|
-
const sets = ["updated_at = ?"];
|
|
9796
|
-
const params = [now()];
|
|
9797
|
-
if (input.name !== undefined) {
|
|
9798
|
-
sets.push("name = ?");
|
|
9799
|
-
params.push(input.name);
|
|
9800
|
-
}
|
|
9801
|
-
if (input.description !== undefined) {
|
|
9802
|
-
sets.push("description = ?");
|
|
9803
|
-
params.push(input.description);
|
|
9804
|
-
}
|
|
9805
|
-
if (input.metadata !== undefined) {
|
|
9806
|
-
sets.push("metadata = ?");
|
|
9807
|
-
params.push(JSON.stringify(input.metadata));
|
|
9808
|
-
}
|
|
9809
|
-
params.push(id);
|
|
9810
|
-
d.run(`UPDATE task_lists SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
9811
|
-
return getTaskList(id, d);
|
|
9812
|
-
}
|
|
9813
|
-
function deleteTaskList(id, db) {
|
|
9814
|
-
const d = db || getDatabase();
|
|
9815
|
-
return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
|
|
9816
|
-
}
|
|
9817
|
-
function ensureTaskList(name, slug, projectId, db) {
|
|
9818
|
-
const d = db || getDatabase();
|
|
9819
|
-
const existing = getTaskListBySlug(slug, projectId, d);
|
|
9820
|
-
if (existing)
|
|
9821
|
-
return existing;
|
|
9822
|
-
return createTaskList({ name, slug, project_id: projectId }, d);
|
|
9823
|
-
}
|
|
9824
|
-
|
|
9825
10302
|
// src/storage/local-sqlite.ts
|
|
9826
10303
|
init_database();
|
|
9827
10304
|
|