@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/server/index.js
CHANGED
|
@@ -1189,6 +1189,49 @@ var init_migrations = __esm(() => {
|
|
|
1189
1189
|
CREATE INDEX IF NOT EXISTS idx_local_retrospectives_plan ON local_retrospectives(plan_id);
|
|
1190
1190
|
CREATE INDEX IF NOT EXISTS idx_local_retrospectives_agent ON local_retrospectives(agent_id);
|
|
1191
1191
|
INSERT OR IGNORE INTO _migrations (id) VALUES (61);
|
|
1192
|
+
`,
|
|
1193
|
+
`
|
|
1194
|
+
CREATE TABLE IF NOT EXISTS task_run_transactions (
|
|
1195
|
+
id TEXT PRIMARY KEY,
|
|
1196
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1197
|
+
run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
|
|
1198
|
+
key TEXT NOT NULL,
|
|
1199
|
+
loop_id TEXT,
|
|
1200
|
+
loop_run_id TEXT,
|
|
1201
|
+
metadata TEXT DEFAULT '{}',
|
|
1202
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1203
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1204
|
+
UNIQUE(task_id, key)
|
|
1205
|
+
);
|
|
1206
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_transactions_task_key ON task_run_transactions(task_id, key);
|
|
1207
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_transactions_key ON task_run_transactions(key);
|
|
1208
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_transactions_run ON task_run_transactions(run_id);
|
|
1209
|
+
|
|
1210
|
+
CREATE TABLE IF NOT EXISTS task_findings (
|
|
1211
|
+
id TEXT PRIMARY KEY,
|
|
1212
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1213
|
+
run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
|
|
1214
|
+
fingerprint TEXT NOT NULL,
|
|
1215
|
+
title TEXT NOT NULL,
|
|
1216
|
+
severity TEXT NOT NULL DEFAULT 'medium' CHECK(severity IN ('low', 'medium', 'high', 'critical')),
|
|
1217
|
+
status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open', 'resolved', 'ignored')),
|
|
1218
|
+
source TEXT,
|
|
1219
|
+
summary TEXT,
|
|
1220
|
+
artifact_path TEXT,
|
|
1221
|
+
metadata TEXT DEFAULT '{}',
|
|
1222
|
+
first_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1223
|
+
last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1224
|
+
resolved_at TEXT,
|
|
1225
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1226
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1227
|
+
UNIQUE(task_id, fingerprint)
|
|
1228
|
+
);
|
|
1229
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_task ON task_findings(task_id);
|
|
1230
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_run ON task_findings(run_id);
|
|
1231
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_status ON task_findings(status);
|
|
1232
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_source ON task_findings(source);
|
|
1233
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_fingerprint ON task_findings(fingerprint);
|
|
1234
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (62);
|
|
1192
1235
|
`
|
|
1193
1236
|
];
|
|
1194
1237
|
});
|
|
@@ -1626,6 +1669,47 @@ function ensureSchema(db) {
|
|
|
1626
1669
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_run ON task_run_artifacts(run_id)");
|
|
1627
1670
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_task ON task_run_artifacts(task_id)");
|
|
1628
1671
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_path ON task_run_artifacts(path)");
|
|
1672
|
+
ensureTable("task_run_transactions", `
|
|
1673
|
+
CREATE TABLE task_run_transactions (
|
|
1674
|
+
id TEXT PRIMARY KEY,
|
|
1675
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1676
|
+
run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
|
|
1677
|
+
key TEXT NOT NULL,
|
|
1678
|
+
loop_id TEXT,
|
|
1679
|
+
loop_run_id TEXT,
|
|
1680
|
+
metadata TEXT DEFAULT '{}',
|
|
1681
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1682
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1683
|
+
UNIQUE(task_id, key)
|
|
1684
|
+
)`);
|
|
1685
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_transactions_task_key ON task_run_transactions(task_id, key)");
|
|
1686
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_transactions_key ON task_run_transactions(key)");
|
|
1687
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_transactions_run ON task_run_transactions(run_id)");
|
|
1688
|
+
ensureTable("task_findings", `
|
|
1689
|
+
CREATE TABLE task_findings (
|
|
1690
|
+
id TEXT PRIMARY KEY,
|
|
1691
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1692
|
+
run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
|
|
1693
|
+
fingerprint TEXT NOT NULL,
|
|
1694
|
+
title TEXT NOT NULL,
|
|
1695
|
+
severity TEXT NOT NULL DEFAULT 'medium' CHECK(severity IN ('low', 'medium', 'high', 'critical')),
|
|
1696
|
+
status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open', 'resolved', 'ignored')),
|
|
1697
|
+
source TEXT,
|
|
1698
|
+
summary TEXT,
|
|
1699
|
+
artifact_path TEXT,
|
|
1700
|
+
metadata TEXT DEFAULT '{}',
|
|
1701
|
+
first_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1702
|
+
last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1703
|
+
resolved_at TEXT,
|
|
1704
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1705
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1706
|
+
UNIQUE(task_id, fingerprint)
|
|
1707
|
+
)`);
|
|
1708
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_task ON task_findings(task_id)");
|
|
1709
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_run ON task_findings(run_id)");
|
|
1710
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_status ON task_findings(status)");
|
|
1711
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_source ON task_findings(source)");
|
|
1712
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_fingerprint ON task_findings(fingerprint)");
|
|
1629
1713
|
ensureTable("inbox_items", `
|
|
1630
1714
|
CREATE TABLE inbox_items (
|
|
1631
1715
|
id TEXT PRIMARY KEY,
|
|
@@ -3994,6 +4078,7 @@ var init_event_hooks = __esm(() => {
|
|
|
3994
4078
|
"task.blocked",
|
|
3995
4079
|
"task.started",
|
|
3996
4080
|
"task.completed",
|
|
4081
|
+
"task.updated",
|
|
3997
4082
|
"task.due",
|
|
3998
4083
|
"task.due_soon",
|
|
3999
4084
|
"task.failed",
|
|
@@ -4014,7 +4099,7 @@ var init_event_hooks = __esm(() => {
|
|
|
4014
4099
|
VALID_TARGETS = new Set(["stdout", "file", "socket", "script"]);
|
|
4015
4100
|
});
|
|
4016
4101
|
|
|
4017
|
-
// node_modules/.bun/@hasna+events@0.1.
|
|
4102
|
+
// node_modules/.bun/@hasna+events@0.1.9/node_modules/@hasna/events/dist/index.js
|
|
4018
4103
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
4019
4104
|
import { existsSync as existsSync6 } from "fs";
|
|
4020
4105
|
import { homedir } from "os";
|
|
@@ -4031,17 +4116,30 @@ function getPathValue(input, path) {
|
|
|
4031
4116
|
return;
|
|
4032
4117
|
}, input);
|
|
4033
4118
|
}
|
|
4034
|
-
function wildcardToRegExp(pattern) {
|
|
4035
|
-
|
|
4036
|
-
|
|
4119
|
+
function wildcardToRegExp(pattern, options = {}) {
|
|
4120
|
+
let body = "";
|
|
4121
|
+
for (let index = 0;index < pattern.length; index += 1) {
|
|
4122
|
+
const char = pattern[index];
|
|
4123
|
+
if (char === "*") {
|
|
4124
|
+
if (pattern[index + 1] === "*") {
|
|
4125
|
+
body += ".*";
|
|
4126
|
+
index += 1;
|
|
4127
|
+
} else {
|
|
4128
|
+
body += options.segmentSafe ? "[^/]*" : ".*";
|
|
4129
|
+
}
|
|
4130
|
+
} else {
|
|
4131
|
+
body += char.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
4132
|
+
}
|
|
4133
|
+
}
|
|
4134
|
+
return new RegExp(`^${body}$`);
|
|
4037
4135
|
}
|
|
4038
|
-
function matchString(value, matcher) {
|
|
4136
|
+
function matchString(value, matcher, options = {}) {
|
|
4039
4137
|
if (matcher === undefined)
|
|
4040
4138
|
return true;
|
|
4041
4139
|
if (value === undefined)
|
|
4042
4140
|
return false;
|
|
4043
4141
|
const matchers = Array.isArray(matcher) ? matcher : [matcher];
|
|
4044
|
-
return matchers.some((item) => wildcardToRegExp(item).test(value));
|
|
4142
|
+
return matchers.some((item) => wildcardToRegExp(item, options).test(value));
|
|
4045
4143
|
}
|
|
4046
4144
|
function matchRecord(input, matcher) {
|
|
4047
4145
|
if (!matcher)
|
|
@@ -4049,7 +4147,9 @@ function matchRecord(input, matcher) {
|
|
|
4049
4147
|
return Object.entries(matcher).every(([path, expected]) => {
|
|
4050
4148
|
const actual = getPathValue(input, path);
|
|
4051
4149
|
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
4052
|
-
return matchString(actual === undefined ? undefined : String(actual), expected
|
|
4150
|
+
return matchString(actual === undefined ? undefined : String(actual), expected, {
|
|
4151
|
+
segmentSafe: path.endsWith("_path") || path.endsWith(".path")
|
|
4152
|
+
});
|
|
4053
4153
|
}
|
|
4054
4154
|
return actual === expected;
|
|
4055
4155
|
});
|
|
@@ -4403,7 +4503,7 @@ class EventsClient {
|
|
|
4403
4503
|
}
|
|
4404
4504
|
return deliveries;
|
|
4405
4505
|
}
|
|
4406
|
-
async
|
|
4506
|
+
async matchChannel(id, input = {}) {
|
|
4407
4507
|
const channel = await this.store.getChannel(id);
|
|
4408
4508
|
if (!channel)
|
|
4409
4509
|
throw new Error(`Channel not found: ${id}`);
|
|
@@ -4420,6 +4520,34 @@ class EventsClient {
|
|
|
4420
4520
|
time: input.time,
|
|
4421
4521
|
id: input.id
|
|
4422
4522
|
});
|
|
4523
|
+
const matched = channelMatchesEvent(channel, event);
|
|
4524
|
+
return {
|
|
4525
|
+
channelId: channel.id,
|
|
4526
|
+
matched,
|
|
4527
|
+
event,
|
|
4528
|
+
filters: channel.filters,
|
|
4529
|
+
reason: matched ? undefined : channel.enabled ? "event did not match channel filters" : "channel is disabled"
|
|
4530
|
+
};
|
|
4531
|
+
}
|
|
4532
|
+
async testChannel(id, input = {}, options = {}) {
|
|
4533
|
+
const channel = await this.store.getChannel(id);
|
|
4534
|
+
if (!channel)
|
|
4535
|
+
throw new Error(`Channel not found: ${id}`);
|
|
4536
|
+
const match = await this.matchChannel(id, input);
|
|
4537
|
+
const event = match.event;
|
|
4538
|
+
if (options.honorFilters && !match.matched) {
|
|
4539
|
+
const timestamp = new Date().toISOString();
|
|
4540
|
+
const result2 = createDeliveryResult(event, channel, [{
|
|
4541
|
+
attempt: 1,
|
|
4542
|
+
status: "skipped",
|
|
4543
|
+
startedAt: timestamp,
|
|
4544
|
+
completedAt: timestamp,
|
|
4545
|
+
error: match.reason
|
|
4546
|
+
}]);
|
|
4547
|
+
result2.metadata = { reason: "filter_mismatch" };
|
|
4548
|
+
await this.store.appendDelivery(result2);
|
|
4549
|
+
return result2;
|
|
4550
|
+
}
|
|
4423
4551
|
const eventForChannel = await this.applyRedaction(event, channel);
|
|
4424
4552
|
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
4425
4553
|
await this.store.appendDelivery(result);
|
|
@@ -4523,6 +4651,90 @@ var init_dist = __esm(() => {
|
|
|
4523
4651
|
DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
4524
4652
|
});
|
|
4525
4653
|
|
|
4654
|
+
// src/db/task-lists.ts
|
|
4655
|
+
function rowToTaskList(row) {
|
|
4656
|
+
return {
|
|
4657
|
+
...row,
|
|
4658
|
+
metadata: JSON.parse(row.metadata || "{}")
|
|
4659
|
+
};
|
|
4660
|
+
}
|
|
4661
|
+
function createTaskList(input, db) {
|
|
4662
|
+
const d = db || getDatabase();
|
|
4663
|
+
const id = uuid();
|
|
4664
|
+
const timestamp = now();
|
|
4665
|
+
const slug = input.slug || slugify(input.name);
|
|
4666
|
+
if (!input.project_id) {
|
|
4667
|
+
const existing = d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ?").get(slug);
|
|
4668
|
+
if (existing) {
|
|
4669
|
+
throw new Error(`Standalone task list with slug "${slug}" already exists`);
|
|
4670
|
+
}
|
|
4671
|
+
}
|
|
4672
|
+
d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at)
|
|
4673
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp]);
|
|
4674
|
+
return getTaskList(id, d);
|
|
4675
|
+
}
|
|
4676
|
+
function getTaskList(id, db) {
|
|
4677
|
+
const d = db || getDatabase();
|
|
4678
|
+
const row = d.query("SELECT * FROM task_lists WHERE id = ?").get(id);
|
|
4679
|
+
return row ? rowToTaskList(row) : null;
|
|
4680
|
+
}
|
|
4681
|
+
function getTaskListBySlug(slug, projectId, db) {
|
|
4682
|
+
const d = db || getDatabase();
|
|
4683
|
+
let row;
|
|
4684
|
+
if (projectId) {
|
|
4685
|
+
row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id = ?").get(slug, projectId);
|
|
4686
|
+
} else {
|
|
4687
|
+
row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id IS NULL").get(slug);
|
|
4688
|
+
}
|
|
4689
|
+
return row ? rowToTaskList(row) : null;
|
|
4690
|
+
}
|
|
4691
|
+
function listTaskLists(projectId, db) {
|
|
4692
|
+
const d = db || getDatabase();
|
|
4693
|
+
if (projectId) {
|
|
4694
|
+
return d.query("SELECT * FROM task_lists WHERE project_id = ? ORDER BY name").all(projectId).map(rowToTaskList);
|
|
4695
|
+
}
|
|
4696
|
+
return d.query("SELECT * FROM task_lists ORDER BY name").all().map(rowToTaskList);
|
|
4697
|
+
}
|
|
4698
|
+
function updateTaskList(id, input, db) {
|
|
4699
|
+
const d = db || getDatabase();
|
|
4700
|
+
const existing = getTaskList(id, d);
|
|
4701
|
+
if (!existing)
|
|
4702
|
+
throw new TaskListNotFoundError(id);
|
|
4703
|
+
const sets = ["updated_at = ?"];
|
|
4704
|
+
const params = [now()];
|
|
4705
|
+
if (input.name !== undefined) {
|
|
4706
|
+
sets.push("name = ?");
|
|
4707
|
+
params.push(input.name);
|
|
4708
|
+
}
|
|
4709
|
+
if (input.description !== undefined) {
|
|
4710
|
+
sets.push("description = ?");
|
|
4711
|
+
params.push(input.description);
|
|
4712
|
+
}
|
|
4713
|
+
if (input.metadata !== undefined) {
|
|
4714
|
+
sets.push("metadata = ?");
|
|
4715
|
+
params.push(JSON.stringify(input.metadata));
|
|
4716
|
+
}
|
|
4717
|
+
params.push(id);
|
|
4718
|
+
d.run(`UPDATE task_lists SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
4719
|
+
return getTaskList(id, d);
|
|
4720
|
+
}
|
|
4721
|
+
function deleteTaskList(id, db) {
|
|
4722
|
+
const d = db || getDatabase();
|
|
4723
|
+
return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
|
|
4724
|
+
}
|
|
4725
|
+
function ensureTaskList(name, slug, projectId, db) {
|
|
4726
|
+
const d = db || getDatabase();
|
|
4727
|
+
const existing = getTaskListBySlug(slug, projectId, d);
|
|
4728
|
+
if (existing)
|
|
4729
|
+
return existing;
|
|
4730
|
+
return createTaskList({ name, slug, project_id: projectId }, d);
|
|
4731
|
+
}
|
|
4732
|
+
var init_task_lists = __esm(() => {
|
|
4733
|
+
init_types();
|
|
4734
|
+
init_database();
|
|
4735
|
+
init_projects();
|
|
4736
|
+
});
|
|
4737
|
+
|
|
4526
4738
|
// src/lib/shared-events.ts
|
|
4527
4739
|
function taskEventData(task, extra = {}) {
|
|
4528
4740
|
return {
|
|
@@ -4552,6 +4764,69 @@ function taskEventData(task, extra = {}) {
|
|
|
4552
4764
|
...extra
|
|
4553
4765
|
};
|
|
4554
4766
|
}
|
|
4767
|
+
function taskEventMetadata(task) {
|
|
4768
|
+
const metadata = {
|
|
4769
|
+
package: "@hasna/todos",
|
|
4770
|
+
todos_event_schema_version: 1,
|
|
4771
|
+
task_id: task.id,
|
|
4772
|
+
task_short_id: task.short_id,
|
|
4773
|
+
project_id: task.project_id,
|
|
4774
|
+
task_list_id: task.task_list_id,
|
|
4775
|
+
working_dir: task.working_dir
|
|
4776
|
+
};
|
|
4777
|
+
try {
|
|
4778
|
+
const project = task.project_id ? getProject(task.project_id) : null;
|
|
4779
|
+
const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
|
|
4780
|
+
if (project) {
|
|
4781
|
+
metadata.project_id = project.id;
|
|
4782
|
+
metadata.project_name = project.name;
|
|
4783
|
+
metadata.project_path = projectPath;
|
|
4784
|
+
metadata.project_canonical_path = project.path;
|
|
4785
|
+
metadata.project_default_task_list_slug = project.task_list_id;
|
|
4786
|
+
metadata.root_project_id = inferRootProjectId(project);
|
|
4787
|
+
} else if (projectPath) {
|
|
4788
|
+
metadata.project_path = projectPath;
|
|
4789
|
+
metadata.project_canonical_path = projectPath;
|
|
4790
|
+
}
|
|
4791
|
+
if (projectPath) {
|
|
4792
|
+
metadata.project_kind = classifyProjectKind(projectPath);
|
|
4793
|
+
metadata.project_is_worktree = isWorktreePath(projectPath);
|
|
4794
|
+
if (typeof task.metadata.route_enabled === "boolean") {
|
|
4795
|
+
metadata.route_enabled = task.metadata.route_enabled;
|
|
4796
|
+
}
|
|
4797
|
+
metadata.working_dir = task.working_dir ?? projectPath;
|
|
4798
|
+
}
|
|
4799
|
+
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;
|
|
4800
|
+
if (taskList) {
|
|
4801
|
+
metadata.task_list_id = taskList.id;
|
|
4802
|
+
metadata.task_list_slug = taskList.slug;
|
|
4803
|
+
metadata.task_list_name = taskList.name;
|
|
4804
|
+
metadata.task_list_project_id = taskList.project_id;
|
|
4805
|
+
metadata.task_list_is_project_default = Boolean(project?.task_list_id && taskList.slug === project.task_list_id);
|
|
4806
|
+
}
|
|
4807
|
+
} catch {}
|
|
4808
|
+
return metadata;
|
|
4809
|
+
}
|
|
4810
|
+
function classifyProjectKind(path) {
|
|
4811
|
+
return path.includes("/hasna/opensource/") ? "open-source" : "unknown";
|
|
4812
|
+
}
|
|
4813
|
+
function isWorktreePath(path) {
|
|
4814
|
+
return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
|
|
4815
|
+
}
|
|
4816
|
+
function inferRootProjectId(project) {
|
|
4817
|
+
return isWorktreePath(project.path) ? null : project.id;
|
|
4818
|
+
}
|
|
4819
|
+
function readMachineLocalPath(project) {
|
|
4820
|
+
const machineId = process.env["TODOS_MACHINE_ID"];
|
|
4821
|
+
if (!machineId)
|
|
4822
|
+
return null;
|
|
4823
|
+
try {
|
|
4824
|
+
const row = getDatabase().query("SELECT path FROM project_machine_paths WHERE project_id = ? AND machine_id = ?").get(project.id, machineId);
|
|
4825
|
+
return row?.path ?? null;
|
|
4826
|
+
} catch {
|
|
4827
|
+
return null;
|
|
4828
|
+
}
|
|
4829
|
+
}
|
|
4555
4830
|
async function emitSharedTaskEvent(input) {
|
|
4556
4831
|
const data = taskEventData(input.task, input.data);
|
|
4557
4832
|
await new EventsClient().emit({
|
|
@@ -4562,12 +4837,7 @@ async function emitSharedTaskEvent(input) {
|
|
|
4562
4837
|
message: input.message ?? `${input.type}: ${input.task.title}`,
|
|
4563
4838
|
data,
|
|
4564
4839
|
dedupeKey: input.dedupeKey ?? `${input.type}:${input.task.id}:${input.task.version}`,
|
|
4565
|
-
metadata:
|
|
4566
|
-
package: "@hasna/todos",
|
|
4567
|
-
task_id: input.task.id,
|
|
4568
|
-
project_id: input.task.project_id,
|
|
4569
|
-
task_list_id: input.task.task_list_id
|
|
4570
|
-
}
|
|
4840
|
+
metadata: taskEventMetadata(input.task)
|
|
4571
4841
|
}, { deliver: true, dedupe: true });
|
|
4572
4842
|
}
|
|
4573
4843
|
function emitSharedTaskEventQuiet(input) {
|
|
@@ -4578,6 +4848,9 @@ function emitSharedTaskEventQuiet(input) {
|
|
|
4578
4848
|
var SOURCE = "todos";
|
|
4579
4849
|
var init_shared_events = __esm(() => {
|
|
4580
4850
|
init_dist();
|
|
4851
|
+
init_database();
|
|
4852
|
+
init_projects();
|
|
4853
|
+
init_task_lists();
|
|
4581
4854
|
});
|
|
4582
4855
|
|
|
4583
4856
|
// src/lib/secret-redaction.ts
|
|
@@ -5102,6 +5375,17 @@ function replaceTaskTags(taskId, tags, db) {
|
|
|
5102
5375
|
db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
|
|
5103
5376
|
insertTaskTags(taskId, tags, db);
|
|
5104
5377
|
}
|
|
5378
|
+
function addMetadataConditions(metadata, conditions, params) {
|
|
5379
|
+
if (!metadata)
|
|
5380
|
+
return;
|
|
5381
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
5382
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(key)) {
|
|
5383
|
+
throw new Error(`Invalid metadata filter key: ${key}`);
|
|
5384
|
+
}
|
|
5385
|
+
conditions.push(`json_extract(metadata, '$."${key}"') = ?`);
|
|
5386
|
+
params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
|
|
5387
|
+
}
|
|
5388
|
+
}
|
|
5105
5389
|
function createTask(input, db) {
|
|
5106
5390
|
const d = db || getDatabase();
|
|
5107
5391
|
const timestamp = now();
|
|
@@ -5284,6 +5568,7 @@ function listTasks(filter = {}, db) {
|
|
|
5284
5568
|
params.push(filter.task_type);
|
|
5285
5569
|
}
|
|
5286
5570
|
}
|
|
5571
|
+
addMetadataConditions(filter.metadata, conditions, params);
|
|
5287
5572
|
const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
|
|
5288
5573
|
if (filter.cursor) {
|
|
5289
5574
|
try {
|
|
@@ -5308,6 +5593,54 @@ function listTasks(filter = {}, db) {
|
|
|
5308
5593
|
const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC${limitClause}`).all(...params);
|
|
5309
5594
|
return rows.map(rowToTask);
|
|
5310
5595
|
}
|
|
5596
|
+
function getTaskByFingerprint(fingerprint, db) {
|
|
5597
|
+
const tasks = listTasks({ metadata: { fingerprint }, limit: 1 }, db);
|
|
5598
|
+
return tasks[0] ?? null;
|
|
5599
|
+
}
|
|
5600
|
+
function mergeTaskMetadata(current, next, fingerprint) {
|
|
5601
|
+
return {
|
|
5602
|
+
...current,
|
|
5603
|
+
...next ?? {},
|
|
5604
|
+
fingerprint
|
|
5605
|
+
};
|
|
5606
|
+
}
|
|
5607
|
+
function upsertTaskByFingerprint(input, db) {
|
|
5608
|
+
const d = db || getDatabase();
|
|
5609
|
+
const fingerprint = input.fingerprint.trim();
|
|
5610
|
+
if (!fingerprint)
|
|
5611
|
+
throw new Error("fingerprint is required");
|
|
5612
|
+
const existing = getTaskByFingerprint(fingerprint, d);
|
|
5613
|
+
const metadata = mergeTaskMetadata(existing?.metadata ?? {}, input.metadata, fingerprint);
|
|
5614
|
+
if (!existing) {
|
|
5615
|
+
const task2 = createTask({ ...input, metadata }, d);
|
|
5616
|
+
return { task: task2, created: true };
|
|
5617
|
+
}
|
|
5618
|
+
const task = updateTask(existing.id, {
|
|
5619
|
+
version: existing.version,
|
|
5620
|
+
title: input.title,
|
|
5621
|
+
description: input.description,
|
|
5622
|
+
status: input.status,
|
|
5623
|
+
priority: input.priority,
|
|
5624
|
+
project_id: input.project_id,
|
|
5625
|
+
assigned_to: input.assigned_to,
|
|
5626
|
+
working_dir: input.working_dir,
|
|
5627
|
+
plan_id: input.plan_id,
|
|
5628
|
+
task_list_id: input.task_list_id,
|
|
5629
|
+
tags: input.tags,
|
|
5630
|
+
metadata,
|
|
5631
|
+
due_at: input.due_at,
|
|
5632
|
+
estimated_minutes: input.estimated_minutes,
|
|
5633
|
+
sla_minutes: input.sla_minutes,
|
|
5634
|
+
confidence: input.confidence,
|
|
5635
|
+
retry_count: input.retry_count,
|
|
5636
|
+
max_retries: input.max_retries,
|
|
5637
|
+
retry_after: input.retry_after,
|
|
5638
|
+
requires_approval: input.requires_approval,
|
|
5639
|
+
recurrence_rule: input.recurrence_rule,
|
|
5640
|
+
task_type: input.task_type
|
|
5641
|
+
}, d);
|
|
5642
|
+
return { task, created: false };
|
|
5643
|
+
}
|
|
5311
5644
|
function countTasks(filter = {}, db) {
|
|
5312
5645
|
const d = db || getDatabase();
|
|
5313
5646
|
const conditions = [];
|
|
@@ -5371,6 +5704,7 @@ function countTasks(filter = {}, db) {
|
|
|
5371
5704
|
conditions.push("task_list_id = ?");
|
|
5372
5705
|
params.push(filter.task_list_id);
|
|
5373
5706
|
}
|
|
5707
|
+
addMetadataConditions(filter.metadata, conditions, params);
|
|
5374
5708
|
if (!filter.include_archived) {
|
|
5375
5709
|
conditions.push("archived_at IS NULL");
|
|
5376
5710
|
}
|
|
@@ -5421,6 +5755,10 @@ function updateTask(id, input, db) {
|
|
|
5421
5755
|
sets.push("assigned_to = ?");
|
|
5422
5756
|
params.push(input.assigned_to);
|
|
5423
5757
|
}
|
|
5758
|
+
if (input.working_dir !== undefined) {
|
|
5759
|
+
sets.push("working_dir = ?");
|
|
5760
|
+
params.push(input.working_dir);
|
|
5761
|
+
}
|
|
5424
5762
|
if (input.tags !== undefined) {
|
|
5425
5763
|
sets.push("tags = ?");
|
|
5426
5764
|
params.push(JSON.stringify(input.tags));
|
|
@@ -5509,6 +5847,8 @@ function updateTask(id, input, db) {
|
|
|
5509
5847
|
logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
|
|
5510
5848
|
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
|
|
5511
5849
|
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
5850
|
+
if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
|
|
5851
|
+
logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
|
|
5512
5852
|
if (input.approved_by !== undefined)
|
|
5513
5853
|
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
5514
5854
|
const updatedTask = {
|
|
@@ -5544,6 +5884,10 @@ function updateTask(id, input, db) {
|
|
|
5544
5884
|
if (input.approved_by !== undefined) {
|
|
5545
5885
|
emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
|
|
5546
5886
|
}
|
|
5887
|
+
const updatePayload = taskEventData(updatedTask);
|
|
5888
|
+
dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
|
|
5889
|
+
emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload });
|
|
5890
|
+
emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask });
|
|
5547
5891
|
return updatedTask;
|
|
5548
5892
|
}
|
|
5549
5893
|
function deleteTask(id, db) {
|
|
@@ -8561,6 +8905,72 @@ function rowToArtifact(row) {
|
|
|
8561
8905
|
function getRunRow(runId, db) {
|
|
8562
8906
|
return db.query("SELECT * FROM task_runs WHERE id = ?").get(runId);
|
|
8563
8907
|
}
|
|
8908
|
+
function normalizeTransactionKey(input) {
|
|
8909
|
+
const key = (input.key || input.loop_run_id || input.loop_id || "").trim();
|
|
8910
|
+
if (!key)
|
|
8911
|
+
throw new Error("idempotent run transactions require --key, --loop-run-id, or --loop-id");
|
|
8912
|
+
return key.toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 240);
|
|
8913
|
+
}
|
|
8914
|
+
function loopTransactionMetadata(record) {
|
|
8915
|
+
const value = record.metadata["loop_transaction"];
|
|
8916
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
8917
|
+
}
|
|
8918
|
+
function runKey(record) {
|
|
8919
|
+
const tx = loopTransactionMetadata(record);
|
|
8920
|
+
const key = tx["idempotency_key"] ?? record.metadata["idempotency_key"];
|
|
8921
|
+
return typeof key === "string" ? key : null;
|
|
8922
|
+
}
|
|
8923
|
+
function loopId(record) {
|
|
8924
|
+
const tx = loopTransactionMetadata(record);
|
|
8925
|
+
const value = tx["loop_id"] ?? record.metadata["loop_id"];
|
|
8926
|
+
return typeof value === "string" ? value : null;
|
|
8927
|
+
}
|
|
8928
|
+
function loopRunId(record) {
|
|
8929
|
+
const tx = loopTransactionMetadata(record);
|
|
8930
|
+
const value = tx["loop_run_id"] ?? record.metadata["loop_run_id"];
|
|
8931
|
+
return typeof value === "string" ? value : null;
|
|
8932
|
+
}
|
|
8933
|
+
function getTaskRunTransactionByKey(key, taskId, db) {
|
|
8934
|
+
if (taskId) {
|
|
8935
|
+
return db.query("SELECT * FROM task_run_transactions WHERE task_id = ? AND key = ?").get(taskId, key);
|
|
8936
|
+
}
|
|
8937
|
+
const rows = db.query("SELECT * FROM task_run_transactions WHERE key = ? ORDER BY created_at DESC LIMIT 2").all(key);
|
|
8938
|
+
if (rows.length > 1)
|
|
8939
|
+
throw new Error(`Run transaction key is ambiguous across tasks: ${key}. Pass task_id.`);
|
|
8940
|
+
return rows[0] ?? null;
|
|
8941
|
+
}
|
|
8942
|
+
function summarizeTaskRun(run) {
|
|
8943
|
+
return {
|
|
8944
|
+
id: run.id,
|
|
8945
|
+
task_id: run.task_id,
|
|
8946
|
+
agent_id: run.agent_id,
|
|
8947
|
+
title: run.title,
|
|
8948
|
+
status: run.status,
|
|
8949
|
+
summary: run.summary,
|
|
8950
|
+
idempotency_key: runKey(run),
|
|
8951
|
+
loop_id: loopId(run),
|
|
8952
|
+
loop_run_id: loopRunId(run),
|
|
8953
|
+
metadata_keys: Object.keys(run.metadata).sort(),
|
|
8954
|
+
started_at: run.started_at,
|
|
8955
|
+
completed_at: run.completed_at,
|
|
8956
|
+
updated_at: run.updated_at
|
|
8957
|
+
};
|
|
8958
|
+
}
|
|
8959
|
+
function findTaskRunByTransactionKey(key, taskId, db) {
|
|
8960
|
+
const d = db || getDatabase();
|
|
8961
|
+
const normalized = normalizeTransactionKey({ key });
|
|
8962
|
+
const transaction = getTaskRunTransactionByKey(normalized, taskId, d);
|
|
8963
|
+
if (transaction?.run_id)
|
|
8964
|
+
return getTaskRun(transaction.run_id, d);
|
|
8965
|
+
return null;
|
|
8966
|
+
}
|
|
8967
|
+
function loopRunCommands(run, key) {
|
|
8968
|
+
return [
|
|
8969
|
+
run ? `todos runs show ${run.id.slice(0, 8)}` : "todos runs list",
|
|
8970
|
+
run ? `todos findings list --task ${run.task_id.slice(0, 8)} --json` : "todos findings list --json",
|
|
8971
|
+
`todos runs begin <task-id> --key ${key} --apply --json`
|
|
8972
|
+
];
|
|
8973
|
+
}
|
|
8564
8974
|
function resolveTaskRunId(idOrPrefix, db) {
|
|
8565
8975
|
const d = db || getDatabase();
|
|
8566
8976
|
const rows = d.query("SELECT id FROM task_runs WHERE id = ? OR id LIKE ? ORDER BY created_at DESC LIMIT 2").all(idOrPrefix, `${idOrPrefix}%`);
|
|
@@ -8579,7 +8989,7 @@ function startTaskRun(input, db) {
|
|
|
8579
8989
|
const d = db || getDatabase();
|
|
8580
8990
|
if (!getTask(input.task_id, d))
|
|
8581
8991
|
throw new TaskNotFoundError(input.task_id);
|
|
8582
|
-
const id = uuid();
|
|
8992
|
+
const id = input.id ?? uuid();
|
|
8583
8993
|
const timestamp = input.started_at || now();
|
|
8584
8994
|
if (input.claim && input.agent_id) {
|
|
8585
8995
|
startTask(input.task_id, input.agent_id, d);
|
|
@@ -8617,6 +9027,97 @@ function startTaskRun(input, db) {
|
|
|
8617
9027
|
emitLocalEventHooksQuiet({ type: "run.started", payload: { id: run.id, task_id: run.task_id, agent_id: run.agent_id, title: run.title } });
|
|
8618
9028
|
return run;
|
|
8619
9029
|
}
|
|
9030
|
+
function beginTaskRunTransaction(input, db) {
|
|
9031
|
+
const d = db || getDatabase();
|
|
9032
|
+
if (!getTask(input.task_id, d))
|
|
9033
|
+
throw new TaskNotFoundError(input.task_id);
|
|
9034
|
+
const timestamp = input.started_at || now();
|
|
9035
|
+
const key = normalizeTransactionKey(input);
|
|
9036
|
+
const existing = findTaskRunByTransactionKey(key, input.task_id, d);
|
|
9037
|
+
const dryRun = !input.apply;
|
|
9038
|
+
if (existing) {
|
|
9039
|
+
return {
|
|
9040
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
9041
|
+
local_only: true,
|
|
9042
|
+
dry_run: dryRun,
|
|
9043
|
+
processed_at: timestamp,
|
|
9044
|
+
action: "matched",
|
|
9045
|
+
key,
|
|
9046
|
+
run: summarizeTaskRun(existing),
|
|
9047
|
+
warnings: existing.status === "running" ? [] : [`matched ${existing.status} run`],
|
|
9048
|
+
commands: loopRunCommands(existing, key)
|
|
9049
|
+
};
|
|
9050
|
+
}
|
|
9051
|
+
if (dryRun) {
|
|
9052
|
+
return {
|
|
9053
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
9054
|
+
local_only: true,
|
|
9055
|
+
dry_run: true,
|
|
9056
|
+
processed_at: timestamp,
|
|
9057
|
+
action: "preview",
|
|
9058
|
+
key,
|
|
9059
|
+
run: null,
|
|
9060
|
+
warnings: [],
|
|
9061
|
+
commands: loopRunCommands(null, key)
|
|
9062
|
+
};
|
|
9063
|
+
}
|
|
9064
|
+
const metadata = redactValue({
|
|
9065
|
+
...input.metadata || {},
|
|
9066
|
+
loop_transaction: {
|
|
9067
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
9068
|
+
idempotency_key: key,
|
|
9069
|
+
loop_id: input.loop_id ?? null,
|
|
9070
|
+
loop_run_id: input.loop_run_id ?? null,
|
|
9071
|
+
first_seen_at: timestamp
|
|
9072
|
+
},
|
|
9073
|
+
idempotency_key: key
|
|
9074
|
+
});
|
|
9075
|
+
const created = d.transaction(() => {
|
|
9076
|
+
d.run(`INSERT OR IGNORE INTO task_run_transactions (
|
|
9077
|
+
id, task_id, run_id, key, loop_id, loop_run_id, metadata, created_at, updated_at
|
|
9078
|
+
) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?)`, [
|
|
9079
|
+
uuid(),
|
|
9080
|
+
input.task_id,
|
|
9081
|
+
key,
|
|
9082
|
+
input.loop_id ?? null,
|
|
9083
|
+
input.loop_run_id ?? null,
|
|
9084
|
+
JSON.stringify(metadata),
|
|
9085
|
+
timestamp,
|
|
9086
|
+
timestamp
|
|
9087
|
+
]);
|
|
9088
|
+
const transaction = getTaskRunTransactionByKey(key, input.task_id, d);
|
|
9089
|
+
if (!transaction)
|
|
9090
|
+
throw new Error(`Could not create run transaction for key: ${key}`);
|
|
9091
|
+
if (transaction.run_id) {
|
|
9092
|
+
const existingRun = getTaskRun(transaction.run_id, d);
|
|
9093
|
+
if (existingRun)
|
|
9094
|
+
return { run: existingRun, action: "matched" };
|
|
9095
|
+
}
|
|
9096
|
+
const run = startTaskRun({
|
|
9097
|
+
id: uuid(),
|
|
9098
|
+
task_id: input.task_id,
|
|
9099
|
+
agent_id: input.agent_id,
|
|
9100
|
+
title: input.title,
|
|
9101
|
+
summary: input.summary,
|
|
9102
|
+
metadata,
|
|
9103
|
+
claim: input.claim,
|
|
9104
|
+
started_at: timestamp
|
|
9105
|
+
}, d);
|
|
9106
|
+
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]);
|
|
9107
|
+
return { run, action: "created" };
|
|
9108
|
+
})();
|
|
9109
|
+
return {
|
|
9110
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
9111
|
+
local_only: true,
|
|
9112
|
+
dry_run: false,
|
|
9113
|
+
processed_at: timestamp,
|
|
9114
|
+
action: created.action,
|
|
9115
|
+
key,
|
|
9116
|
+
run: summarizeTaskRun(created.run),
|
|
9117
|
+
warnings: [],
|
|
9118
|
+
commands: loopRunCommands(created.run, key)
|
|
9119
|
+
};
|
|
9120
|
+
}
|
|
8620
9121
|
function addTaskRunEvent(input, db) {
|
|
8621
9122
|
const d = db || getDatabase();
|
|
8622
9123
|
const runId = resolveTaskRunId(input.run_id, d);
|
|
@@ -8796,6 +9297,66 @@ function finishTaskRun(input, db) {
|
|
|
8796
9297
|
});
|
|
8797
9298
|
return updated;
|
|
8798
9299
|
}
|
|
9300
|
+
function finishTaskRunTransaction(input, db) {
|
|
9301
|
+
const d = db || getDatabase();
|
|
9302
|
+
const timestamp = input.completed_at || now();
|
|
9303
|
+
const status = input.status || "completed";
|
|
9304
|
+
const key = input.key ? normalizeTransactionKey({ key: input.key }) : "";
|
|
9305
|
+
const run = input.run_id ? getTaskRun(resolveTaskRunId(input.run_id, d), d) : key ? findTaskRunByTransactionKey(key, input.task_id, d) : null;
|
|
9306
|
+
if (!run) {
|
|
9307
|
+
throw new Error(input.run_id ? `Run not found: ${input.run_id}` : "runs finish requires a run id or --key");
|
|
9308
|
+
}
|
|
9309
|
+
if (input.task_id && run.task_id !== input.task_id) {
|
|
9310
|
+
throw new Error(`Run ${run.id} belongs to task ${run.task_id}, not ${input.task_id}`);
|
|
9311
|
+
}
|
|
9312
|
+
const resolvedKey = key || runKey(run) || run.id;
|
|
9313
|
+
const dryRun = input.apply === false;
|
|
9314
|
+
if (run.status !== "running") {
|
|
9315
|
+
const conflict = run.status !== status;
|
|
9316
|
+
return {
|
|
9317
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
9318
|
+
local_only: true,
|
|
9319
|
+
dry_run: dryRun,
|
|
9320
|
+
processed_at: timestamp,
|
|
9321
|
+
action: conflict ? "conflict" : "matched",
|
|
9322
|
+
key: resolvedKey,
|
|
9323
|
+
run: summarizeTaskRun(run),
|
|
9324
|
+
warnings: conflict ? [`run is already ${run.status}; requested ${status}`] : [],
|
|
9325
|
+
commands: loopRunCommands(run, resolvedKey)
|
|
9326
|
+
};
|
|
9327
|
+
}
|
|
9328
|
+
if (dryRun) {
|
|
9329
|
+
return {
|
|
9330
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
9331
|
+
local_only: true,
|
|
9332
|
+
dry_run: true,
|
|
9333
|
+
processed_at: timestamp,
|
|
9334
|
+
action: "preview",
|
|
9335
|
+
key: resolvedKey,
|
|
9336
|
+
run: summarizeTaskRun({ ...run, status, summary: input.summary ?? run.summary, completed_at: timestamp, updated_at: timestamp }),
|
|
9337
|
+
warnings: [],
|
|
9338
|
+
commands: loopRunCommands(run, resolvedKey)
|
|
9339
|
+
};
|
|
9340
|
+
}
|
|
9341
|
+
const finished = finishTaskRun({
|
|
9342
|
+
run_id: run.id,
|
|
9343
|
+
status,
|
|
9344
|
+
summary: input.summary,
|
|
9345
|
+
agent_id: input.agent_id,
|
|
9346
|
+
completed_at: timestamp
|
|
9347
|
+
}, d);
|
|
9348
|
+
return {
|
|
9349
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
9350
|
+
local_only: true,
|
|
9351
|
+
dry_run: false,
|
|
9352
|
+
processed_at: timestamp,
|
|
9353
|
+
action: "finished",
|
|
9354
|
+
key: resolvedKey,
|
|
9355
|
+
run: summarizeTaskRun(finished),
|
|
9356
|
+
warnings: [],
|
|
9357
|
+
commands: loopRunCommands(finished, resolvedKey)
|
|
9358
|
+
};
|
|
9359
|
+
}
|
|
8799
9360
|
function listTaskRuns(taskId, db) {
|
|
8800
9361
|
const d = db || getDatabase();
|
|
8801
9362
|
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();
|
|
@@ -8813,6 +9374,7 @@ function getTaskRunLedger(runId, db) {
|
|
|
8813
9374
|
const files = d.query("SELECT * FROM task_files WHERE task_id = ? ORDER BY updated_at DESC, path").all(run.task_id);
|
|
8814
9375
|
return { run, events, commands, artifacts, files };
|
|
8815
9376
|
}
|
|
9377
|
+
var LOOP_RUN_TRANSACTION_SCHEMA_VERSION = "todos.loop_run_transaction.v1";
|
|
8816
9378
|
var init_task_runs = __esm(() => {
|
|
8817
9379
|
init_artifact_store();
|
|
8818
9380
|
init_event_hooks();
|
|
@@ -9201,6 +9763,7 @@ var init_calendar = __esm(() => {
|
|
|
9201
9763
|
var exports_tasks = {};
|
|
9202
9764
|
__export(exports_tasks, {
|
|
9203
9765
|
watchTask: () => watchTask,
|
|
9766
|
+
upsertTaskByFingerprint: () => upsertTaskByFingerprint,
|
|
9204
9767
|
updateTaskBoard: () => updateTaskBoard,
|
|
9205
9768
|
updateTask: () => updateTask,
|
|
9206
9769
|
unwatchTask: () => unwatchTask,
|
|
@@ -9244,6 +9807,7 @@ __export(exports_tasks, {
|
|
|
9244
9807
|
getTaskGraph: () => getTaskGraph,
|
|
9245
9808
|
getTaskDependents: () => getTaskDependents,
|
|
9246
9809
|
getTaskDependencies: () => getTaskDependencies,
|
|
9810
|
+
getTaskByFingerprint: () => getTaskByFingerprint,
|
|
9247
9811
|
getTaskBoard: () => getTaskBoard,
|
|
9248
9812
|
getTask: () => getTask,
|
|
9249
9813
|
getStatus: () => getStatus,
|
|
@@ -10637,6 +11201,39 @@ async function handleCreateTask(req, ctx, json2, taskToSummary2) {
|
|
|
10637
11201
|
return json2({ error: e instanceof Error ? e.message : "Failed to create task" }, 500);
|
|
10638
11202
|
}
|
|
10639
11203
|
}
|
|
11204
|
+
async function handleUpsertTask(req, ctx, json2, taskToSummary2) {
|
|
11205
|
+
try {
|
|
11206
|
+
const body = await req.json();
|
|
11207
|
+
if (typeof body["fingerprint"] !== "string" || body["fingerprint"].trim() === "") {
|
|
11208
|
+
return json2({ error: "Missing 'fingerprint'" }, 400);
|
|
11209
|
+
}
|
|
11210
|
+
if (typeof body["title"] !== "string" || body["title"].trim() === "") {
|
|
11211
|
+
return json2({ error: "Missing 'title'" }, 400);
|
|
11212
|
+
}
|
|
11213
|
+
const metadata = body["metadata"] && typeof body["metadata"] === "object" && !Array.isArray(body["metadata"]) ? { ...body["metadata"] } : {};
|
|
11214
|
+
for (const key of ["expectation_id", "expectation_fingerprint", "evidence_paths", "origin_loop_id", "origin_run_id", "expected", "observed", "acceptance"]) {
|
|
11215
|
+
if (body[key] !== undefined)
|
|
11216
|
+
metadata[key] = body[key];
|
|
11217
|
+
}
|
|
11218
|
+
const result = upsertTaskByFingerprint({
|
|
11219
|
+
fingerprint: body["fingerprint"],
|
|
11220
|
+
title: body["title"],
|
|
11221
|
+
description: typeof body["description"] === "string" ? body["description"] : undefined,
|
|
11222
|
+
status: body["status"],
|
|
11223
|
+
priority: body["priority"],
|
|
11224
|
+
project_id: typeof body["project_id"] === "string" ? body["project_id"] : undefined,
|
|
11225
|
+
task_list_id: typeof body["task_list_id"] === "string" ? body["task_list_id"] : undefined,
|
|
11226
|
+
assigned_to: typeof body["assigned_to"] === "string" ? body["assigned_to"] : undefined,
|
|
11227
|
+
working_dir: typeof body["working_dir"] === "string" ? body["working_dir"] : undefined,
|
|
11228
|
+
tags: Array.isArray(body["tags"]) ? body["tags"].filter((tag) => typeof tag === "string") : undefined,
|
|
11229
|
+
metadata
|
|
11230
|
+
});
|
|
11231
|
+
ctx.broadcastEvent({ type: "task", task_id: result.task.id, action: result.created ? "created" : "updated", agent_id: result.task.agent_id, project_id: result.task.project_id });
|
|
11232
|
+
return json2({ created: result.created, task: taskToSummary2(result.task) }, result.created ? 201 : 200);
|
|
11233
|
+
} catch (e) {
|
|
11234
|
+
return json2({ error: e instanceof Error ? e.message : "Failed to upsert task" }, 500);
|
|
11235
|
+
}
|
|
11236
|
+
}
|
|
10640
11237
|
function handleTasksExport(_req, url, _ctx, _json, taskToSummary2) {
|
|
10641
11238
|
const format = url.searchParams.get("format") || "json";
|
|
10642
11239
|
const status = url.searchParams.get("status") || undefined;
|
|
@@ -31823,90 +32420,6 @@ var init_dispatch = __esm(() => {
|
|
|
31823
32420
|
init_tmux();
|
|
31824
32421
|
});
|
|
31825
32422
|
|
|
31826
|
-
// src/db/task-lists.ts
|
|
31827
|
-
function rowToTaskList(row) {
|
|
31828
|
-
return {
|
|
31829
|
-
...row,
|
|
31830
|
-
metadata: JSON.parse(row.metadata || "{}")
|
|
31831
|
-
};
|
|
31832
|
-
}
|
|
31833
|
-
function createTaskList(input, db) {
|
|
31834
|
-
const d = db || getDatabase();
|
|
31835
|
-
const id = uuid();
|
|
31836
|
-
const timestamp = now();
|
|
31837
|
-
const slug = input.slug || slugify(input.name);
|
|
31838
|
-
if (!input.project_id) {
|
|
31839
|
-
const existing = d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ?").get(slug);
|
|
31840
|
-
if (existing) {
|
|
31841
|
-
throw new Error(`Standalone task list with slug "${slug}" already exists`);
|
|
31842
|
-
}
|
|
31843
|
-
}
|
|
31844
|
-
d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at)
|
|
31845
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp]);
|
|
31846
|
-
return getTaskList(id, d);
|
|
31847
|
-
}
|
|
31848
|
-
function getTaskList(id, db) {
|
|
31849
|
-
const d = db || getDatabase();
|
|
31850
|
-
const row = d.query("SELECT * FROM task_lists WHERE id = ?").get(id);
|
|
31851
|
-
return row ? rowToTaskList(row) : null;
|
|
31852
|
-
}
|
|
31853
|
-
function getTaskListBySlug(slug, projectId, db) {
|
|
31854
|
-
const d = db || getDatabase();
|
|
31855
|
-
let row;
|
|
31856
|
-
if (projectId) {
|
|
31857
|
-
row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id = ?").get(slug, projectId);
|
|
31858
|
-
} else {
|
|
31859
|
-
row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id IS NULL").get(slug);
|
|
31860
|
-
}
|
|
31861
|
-
return row ? rowToTaskList(row) : null;
|
|
31862
|
-
}
|
|
31863
|
-
function listTaskLists(projectId, db) {
|
|
31864
|
-
const d = db || getDatabase();
|
|
31865
|
-
if (projectId) {
|
|
31866
|
-
return d.query("SELECT * FROM task_lists WHERE project_id = ? ORDER BY name").all(projectId).map(rowToTaskList);
|
|
31867
|
-
}
|
|
31868
|
-
return d.query("SELECT * FROM task_lists ORDER BY name").all().map(rowToTaskList);
|
|
31869
|
-
}
|
|
31870
|
-
function updateTaskList(id, input, db) {
|
|
31871
|
-
const d = db || getDatabase();
|
|
31872
|
-
const existing = getTaskList(id, d);
|
|
31873
|
-
if (!existing)
|
|
31874
|
-
throw new TaskListNotFoundError(id);
|
|
31875
|
-
const sets = ["updated_at = ?"];
|
|
31876
|
-
const params = [now()];
|
|
31877
|
-
if (input.name !== undefined) {
|
|
31878
|
-
sets.push("name = ?");
|
|
31879
|
-
params.push(input.name);
|
|
31880
|
-
}
|
|
31881
|
-
if (input.description !== undefined) {
|
|
31882
|
-
sets.push("description = ?");
|
|
31883
|
-
params.push(input.description);
|
|
31884
|
-
}
|
|
31885
|
-
if (input.metadata !== undefined) {
|
|
31886
|
-
sets.push("metadata = ?");
|
|
31887
|
-
params.push(JSON.stringify(input.metadata));
|
|
31888
|
-
}
|
|
31889
|
-
params.push(id);
|
|
31890
|
-
d.run(`UPDATE task_lists SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
31891
|
-
return getTaskList(id, d);
|
|
31892
|
-
}
|
|
31893
|
-
function deleteTaskList(id, db) {
|
|
31894
|
-
const d = db || getDatabase();
|
|
31895
|
-
return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
|
|
31896
|
-
}
|
|
31897
|
-
function ensureTaskList(name, slug, projectId, db) {
|
|
31898
|
-
const d = db || getDatabase();
|
|
31899
|
-
const existing = getTaskListBySlug(slug, projectId, d);
|
|
31900
|
-
if (existing)
|
|
31901
|
-
return existing;
|
|
31902
|
-
return createTaskList({ name, slug, project_id: projectId }, d);
|
|
31903
|
-
}
|
|
31904
|
-
var init_task_lists = __esm(() => {
|
|
31905
|
-
init_types();
|
|
31906
|
-
init_database();
|
|
31907
|
-
init_projects();
|
|
31908
|
-
});
|
|
31909
|
-
|
|
31910
32423
|
// src/mcp/tools/dispatch.ts
|
|
31911
32424
|
function registerDispatchTools(server, { shouldRegisterTool, resolveId, formatError: formatError2 }) {
|
|
31912
32425
|
if (shouldRegisterTool("dispatch_tasks")) {
|
|
@@ -32465,6 +32978,7 @@ var init_token_utils = __esm(() => {
|
|
|
32465
32978
|
"add_task_run_event",
|
|
32466
32979
|
"add_task_run_file",
|
|
32467
32980
|
"acknowledge_handoff",
|
|
32981
|
+
"begin_task_run_transaction",
|
|
32468
32982
|
"build_local_report",
|
|
32469
32983
|
"cancel_agent_run_dispatch",
|
|
32470
32984
|
"finish_task_run",
|
|
@@ -32512,6 +33026,7 @@ var init_token_utils = __esm(() => {
|
|
|
32512
33026
|
"list_local_snapshots",
|
|
32513
33027
|
"list_retrospectives",
|
|
32514
33028
|
"list_risks",
|
|
33029
|
+
"list_task_findings",
|
|
32515
33030
|
"list_task_runs",
|
|
32516
33031
|
"list_verification_providers",
|
|
32517
33032
|
"merge_duplicate_task",
|
|
@@ -32520,6 +33035,7 @@ var init_token_utils = __esm(() => {
|
|
|
32520
33035
|
"remove_review_routing_rule",
|
|
32521
33036
|
"restore_local_backup",
|
|
32522
33037
|
"retry_agent_run_dispatch",
|
|
33038
|
+
"resolve_missing_task_findings",
|
|
32523
33039
|
"resolve_mentions",
|
|
32524
33040
|
"run_next_agent_dispatch",
|
|
32525
33041
|
"search_knowledge_records",
|
|
@@ -32562,9 +33078,17 @@ var init_token_utils = __esm(() => {
|
|
|
32562
33078
|
"unlock_file",
|
|
32563
33079
|
"unwatch_task",
|
|
32564
33080
|
"update_comment",
|
|
33081
|
+
"upsert_task_finding",
|
|
32565
33082
|
"update_risk",
|
|
32566
33083
|
"watch_task"
|
|
32567
33084
|
],
|
|
33085
|
+
loops: [
|
|
33086
|
+
"begin_task_run_transaction",
|
|
33087
|
+
"finish_task_run",
|
|
33088
|
+
"list_task_findings",
|
|
33089
|
+
"resolve_missing_task_findings",
|
|
33090
|
+
"upsert_task_finding"
|
|
33091
|
+
],
|
|
32568
33092
|
agents: [
|
|
32569
33093
|
"auto_assign_task",
|
|
32570
33094
|
"delete_agent",
|
|
@@ -32646,7 +33170,7 @@ var init_token_utils = __esm(() => {
|
|
|
32646
33170
|
maintenance: ["extract_todos", "get_sla_breaches", "notify_upcoming_deadlines", "run_doctor", "score_task", "watch_source_todos"]
|
|
32647
33171
|
};
|
|
32648
33172
|
MCP_PROFILE_GROUPS = {
|
|
32649
|
-
minimal: ["core"],
|
|
33173
|
+
minimal: ["core", "loops"],
|
|
32650
33174
|
core: ["core"],
|
|
32651
33175
|
standard: ["core", "tasks", "projects", "resources", "agents", "metadata"],
|
|
32652
33176
|
agent: ["core", "tasks", "projects", "resources"],
|
|
@@ -32726,6 +33250,61 @@ function registerTaskCrudTools(server, ctx) {
|
|
|
32726
33250
|
}
|
|
32727
33251
|
});
|
|
32728
33252
|
}
|
|
33253
|
+
if (shouldRegisterTool("upsert_task")) {
|
|
33254
|
+
server.tool("upsert_task", "Create or update a task by stable metadata fingerprint. Metadata is shallow-merged on updates.", {
|
|
33255
|
+
fingerprint: exports_external.string().describe("Stable dedupe fingerprint stored as metadata.fingerprint"),
|
|
33256
|
+
title: exports_external.string().describe("Task title"),
|
|
33257
|
+
description: exports_external.string().optional().describe("Task description"),
|
|
33258
|
+
status: exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]).optional(),
|
|
33259
|
+
priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
|
|
33260
|
+
project_id: exports_external.string().optional().describe("Project ID"),
|
|
33261
|
+
task_list_id: exports_external.string().optional().describe("Task list ID"),
|
|
33262
|
+
assigned_to: exports_external.string().optional().describe("Agent ID or name to assign to"),
|
|
33263
|
+
tags: exports_external.array(exports_external.string()).optional().describe("Tags for the task"),
|
|
33264
|
+
working_dir: exports_external.string().optional().describe("Working directory associated with the task"),
|
|
33265
|
+
metadata: exports_external.record(exports_external.unknown()).optional().describe("Metadata object to shallow-merge"),
|
|
33266
|
+
expectation_id: exports_external.string().optional(),
|
|
33267
|
+
expectation_fingerprint: exports_external.string().optional(),
|
|
33268
|
+
evidence_paths: exports_external.array(exports_external.string()).optional(),
|
|
33269
|
+
origin_loop_id: exports_external.string().optional(),
|
|
33270
|
+
origin_run_id: exports_external.string().optional(),
|
|
33271
|
+
expected: exports_external.unknown().optional(),
|
|
33272
|
+
observed: exports_external.unknown().optional(),
|
|
33273
|
+
acceptance: exports_external.unknown().optional()
|
|
33274
|
+
}, async (params) => {
|
|
33275
|
+
try {
|
|
33276
|
+
const { assigned_to, project_id, task_list_id, metadata, expectation_id, expectation_fingerprint, evidence_paths, origin_loop_id, origin_run_id, expected, observed, acceptance, ...rest } = params;
|
|
33277
|
+
const mergedMetadata = { ...metadata ?? {} };
|
|
33278
|
+
if (expectation_id !== undefined)
|
|
33279
|
+
mergedMetadata["expectation_id"] = expectation_id;
|
|
33280
|
+
if (expectation_fingerprint !== undefined)
|
|
33281
|
+
mergedMetadata["expectation_fingerprint"] = expectation_fingerprint;
|
|
33282
|
+
if (evidence_paths !== undefined)
|
|
33283
|
+
mergedMetadata["evidence_paths"] = evidence_paths;
|
|
33284
|
+
if (origin_loop_id !== undefined)
|
|
33285
|
+
mergedMetadata["origin_loop_id"] = origin_loop_id;
|
|
33286
|
+
if (origin_run_id !== undefined)
|
|
33287
|
+
mergedMetadata["origin_run_id"] = origin_run_id;
|
|
33288
|
+
if (expected !== undefined)
|
|
33289
|
+
mergedMetadata["expected"] = expected;
|
|
33290
|
+
if (observed !== undefined)
|
|
33291
|
+
mergedMetadata["observed"] = observed;
|
|
33292
|
+
if (acceptance !== undefined)
|
|
33293
|
+
mergedMetadata["acceptance"] = acceptance;
|
|
33294
|
+
const resolved = { ...rest, metadata: mergedMetadata };
|
|
33295
|
+
if (assigned_to)
|
|
33296
|
+
resolved.assigned_to = resolveAssignee(assigned_to);
|
|
33297
|
+
if (project_id)
|
|
33298
|
+
resolved.project_id = resolveId(project_id, "projects");
|
|
33299
|
+
if (task_list_id)
|
|
33300
|
+
resolved.task_list_id = resolveId(task_list_id, "task_lists");
|
|
33301
|
+
const result = upsertTaskByFingerprint(resolved);
|
|
33302
|
+
return { content: [{ type: "text", text: compactJson({ created: result.created, task: JSON.parse(mutationTaskResponse(result.task)) }) }] };
|
|
33303
|
+
} catch (e) {
|
|
33304
|
+
return { content: [{ type: "text", text: formatError2(e) }], isError: true };
|
|
33305
|
+
}
|
|
33306
|
+
});
|
|
33307
|
+
}
|
|
32729
33308
|
if (shouldRegisterTool("list_tasks")) {
|
|
32730
33309
|
server.tool("list_tasks", "List tasks with optional filters. Pass empty arrays for multi-value filters (e.g. status=[] shows all).", {
|
|
32731
33310
|
status: exports_external.union([exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]), exports_external.array(exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]))]).optional().describe("Filter by status"),
|
|
@@ -32737,7 +33316,8 @@ function registerTaskCrudTools(server, ctx) {
|
|
|
32737
33316
|
created_after: exports_external.string().optional().describe("ISO date \u2014 tasks created after this date"),
|
|
32738
33317
|
created_before: exports_external.string().optional().describe("ISO date \u2014 tasks created before this date"),
|
|
32739
33318
|
limit: exports_external.number().optional().describe("Max results (default: 50, max 500)"),
|
|
32740
|
-
offset: exports_external.number().optional().describe("Pagination offset")
|
|
33319
|
+
offset: exports_external.number().optional().describe("Pagination offset"),
|
|
33320
|
+
metadata: exports_external.record(exports_external.unknown()).optional().describe("Exact top-level metadata filters")
|
|
32741
33321
|
}, async (params) => {
|
|
32742
33322
|
try {
|
|
32743
33323
|
const resolved = { ...params };
|
|
@@ -36278,7 +36858,7 @@ function countMovedStandaloneVerifications(db, duplicateId) {
|
|
|
36278
36858
|
const rows = db.query("SELECT command FROM task_verifications WHERE task_id = ?").all(duplicateId);
|
|
36279
36859
|
return rows.filter((row) => !generatedCommands.has(row.command)).length;
|
|
36280
36860
|
}
|
|
36281
|
-
function
|
|
36861
|
+
function mergeTaskMetadata2(primary, duplicate, input, mergedAt) {
|
|
36282
36862
|
const mergedDuplicates = Array.isArray(primary.metadata["merged_duplicates"]) ? [...primary.metadata["merged_duplicates"]] : [];
|
|
36283
36863
|
mergedDuplicates.push({
|
|
36284
36864
|
id: duplicate.id,
|
|
@@ -36339,7 +36919,7 @@ function mergeDuplicateTask(input, db) {
|
|
|
36339
36919
|
updateTask(primary.id, {
|
|
36340
36920
|
version: primary.version,
|
|
36341
36921
|
tags: mergedTags,
|
|
36342
|
-
metadata:
|
|
36922
|
+
metadata: mergeTaskMetadata2(primary, duplicate, input, mergedAt),
|
|
36343
36923
|
description: mergeTaskDescription(primary, duplicate) ?? undefined
|
|
36344
36924
|
}, d);
|
|
36345
36925
|
moved.comments = updateRows(d, "task_comments", "task_id", duplicate.id, primary.id);
|
|
@@ -66516,6 +67096,356 @@ var init_task_meta_tools = __esm(() => {
|
|
|
66516
67096
|
init_zod();
|
|
66517
67097
|
});
|
|
66518
67098
|
|
|
67099
|
+
// src/db/findings.ts
|
|
67100
|
+
function parseObject2(value) {
|
|
67101
|
+
if (!value)
|
|
67102
|
+
return {};
|
|
67103
|
+
try {
|
|
67104
|
+
const parsed = JSON.parse(value);
|
|
67105
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
67106
|
+
} catch {
|
|
67107
|
+
return {};
|
|
67108
|
+
}
|
|
67109
|
+
}
|
|
67110
|
+
function normalizeKey(value) {
|
|
67111
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
67112
|
+
}
|
|
67113
|
+
function normalizeFingerprint(value) {
|
|
67114
|
+
const normalized = normalizeKey(value);
|
|
67115
|
+
if (!normalized)
|
|
67116
|
+
throw new Error("finding fingerprint is required");
|
|
67117
|
+
return normalized.slice(0, 240);
|
|
67118
|
+
}
|
|
67119
|
+
function normalizeSeverity(value) {
|
|
67120
|
+
const normalized = normalizeKey(value || "medium");
|
|
67121
|
+
if (SEVERITIES.has(normalized))
|
|
67122
|
+
return normalized;
|
|
67123
|
+
if (/^(p0|blocker|urgent|highest)$/.test(normalized))
|
|
67124
|
+
return "critical";
|
|
67125
|
+
if (/^(p1|major)$/.test(normalized))
|
|
67126
|
+
return "high";
|
|
67127
|
+
if (/^(p3|minor|info)$/.test(normalized))
|
|
67128
|
+
return "low";
|
|
67129
|
+
return "medium";
|
|
67130
|
+
}
|
|
67131
|
+
function normalizeStatus(value) {
|
|
67132
|
+
const normalized = normalizeKey(value || "open");
|
|
67133
|
+
if (STATUSES.has(normalized))
|
|
67134
|
+
return normalized;
|
|
67135
|
+
if (normalized === "closed" || normalized === "fixed")
|
|
67136
|
+
return "resolved";
|
|
67137
|
+
return "open";
|
|
67138
|
+
}
|
|
67139
|
+
function normalizeResolutionStatus(value) {
|
|
67140
|
+
const status = normalizeStatus(value || "resolved");
|
|
67141
|
+
if (status === "open")
|
|
67142
|
+
throw new Error("resolve-missing status must be resolved or ignored");
|
|
67143
|
+
return status;
|
|
67144
|
+
}
|
|
67145
|
+
function redactOptional(value, max = 2000) {
|
|
67146
|
+
if (!value)
|
|
67147
|
+
return null;
|
|
67148
|
+
const redacted = redactEvidenceText(value).trim();
|
|
67149
|
+
if (!redacted)
|
|
67150
|
+
return null;
|
|
67151
|
+
return redacted.length > max ? `${redacted.slice(0, max - 3)}...` : redacted;
|
|
67152
|
+
}
|
|
67153
|
+
function rowToFinding(row) {
|
|
67154
|
+
return {
|
|
67155
|
+
schema_version: TASK_FINDING_SCHEMA_VERSION,
|
|
67156
|
+
...row,
|
|
67157
|
+
severity: normalizeSeverity(row.severity),
|
|
67158
|
+
status: normalizeStatus(row.status),
|
|
67159
|
+
metadata: parseObject2(row.metadata)
|
|
67160
|
+
};
|
|
67161
|
+
}
|
|
67162
|
+
function compactFinding(finding2) {
|
|
67163
|
+
return {
|
|
67164
|
+
schema_version: TASK_FINDING_SCHEMA_VERSION,
|
|
67165
|
+
id: finding2.id,
|
|
67166
|
+
task_id: finding2.task_id,
|
|
67167
|
+
run_id: finding2.run_id,
|
|
67168
|
+
fingerprint: finding2.fingerprint,
|
|
67169
|
+
title: finding2.title,
|
|
67170
|
+
severity: finding2.severity,
|
|
67171
|
+
status: finding2.status,
|
|
67172
|
+
source: finding2.source,
|
|
67173
|
+
summary: finding2.summary,
|
|
67174
|
+
artifact_path: finding2.artifact_path,
|
|
67175
|
+
first_seen_at: finding2.first_seen_at,
|
|
67176
|
+
last_seen_at: finding2.last_seen_at,
|
|
67177
|
+
resolved_at: finding2.resolved_at,
|
|
67178
|
+
metadata_keys: Object.keys(finding2.metadata).sort()
|
|
67179
|
+
};
|
|
67180
|
+
}
|
|
67181
|
+
function previewFinding(existing, next, timestamp3) {
|
|
67182
|
+
return {
|
|
67183
|
+
...existing,
|
|
67184
|
+
run_id: next.run_id,
|
|
67185
|
+
title: next.title,
|
|
67186
|
+
severity: next.severity,
|
|
67187
|
+
status: next.status,
|
|
67188
|
+
source: next.source,
|
|
67189
|
+
summary: next.summary,
|
|
67190
|
+
artifact_path: next.artifact_path,
|
|
67191
|
+
metadata: next.metadata,
|
|
67192
|
+
last_seen_at: timestamp3,
|
|
67193
|
+
resolved_at: next.status === "open" ? null : existing.resolved_at || timestamp3,
|
|
67194
|
+
updated_at: timestamp3
|
|
67195
|
+
};
|
|
67196
|
+
}
|
|
67197
|
+
function upsertAction(existing, next) {
|
|
67198
|
+
if (sameFinding(existing, next))
|
|
67199
|
+
return "matched";
|
|
67200
|
+
return existing.status !== "open" && next.status === "open" ? "reopened" : "updated";
|
|
67201
|
+
}
|
|
67202
|
+
function resolveRunForTask(runId, taskId, db) {
|
|
67203
|
+
if (!runId)
|
|
67204
|
+
return null;
|
|
67205
|
+
const resolved = resolveTaskRunId(runId, db);
|
|
67206
|
+
const run = getTaskRun(resolved, db);
|
|
67207
|
+
if (!run)
|
|
67208
|
+
throw new Error(`Run not found: ${runId}`);
|
|
67209
|
+
if (run.task_id !== taskId)
|
|
67210
|
+
throw new Error(`Run ${resolved} belongs to task ${run.task_id}, not ${taskId}`);
|
|
67211
|
+
return resolved;
|
|
67212
|
+
}
|
|
67213
|
+
function getFindingByFingerprint(taskId, fingerprint2, db) {
|
|
67214
|
+
const row = db.query("SELECT * FROM task_findings WHERE task_id = ? AND fingerprint = ?").get(taskId, fingerprint2);
|
|
67215
|
+
return row ? rowToFinding(row) : null;
|
|
67216
|
+
}
|
|
67217
|
+
function assertTask(taskId, db) {
|
|
67218
|
+
if (!getTask(taskId, db))
|
|
67219
|
+
throw new TaskNotFoundError(taskId);
|
|
67220
|
+
}
|
|
67221
|
+
function nextFinding(input, db) {
|
|
67222
|
+
const fingerprint2 = normalizeFingerprint(input.fingerprint);
|
|
67223
|
+
const title = redactOptional(input.title, 300);
|
|
67224
|
+
if (!title)
|
|
67225
|
+
throw new Error("finding title is required");
|
|
67226
|
+
return {
|
|
67227
|
+
fingerprint: fingerprint2,
|
|
67228
|
+
run_id: resolveRunForTask(input.run_id, input.task_id, db),
|
|
67229
|
+
title,
|
|
67230
|
+
severity: normalizeSeverity(input.severity),
|
|
67231
|
+
status: normalizeStatus(input.status),
|
|
67232
|
+
source: redactOptional(input.source, 120),
|
|
67233
|
+
summary: redactOptional(input.summary, 2000),
|
|
67234
|
+
artifact_path: redactOptional(input.artifact_path, 1000),
|
|
67235
|
+
metadata: redactValue(input.metadata || {})
|
|
67236
|
+
};
|
|
67237
|
+
}
|
|
67238
|
+
function sameFinding(left, right) {
|
|
67239
|
+
return left.run_id === right.run_id && left.title === right.title && left.severity === right.severity && left.status === right.status && left.source === right.source && left.summary === right.summary && left.artifact_path === right.artifact_path && JSON.stringify(left.metadata) === JSON.stringify(right.metadata);
|
|
67240
|
+
}
|
|
67241
|
+
function upsertTaskFinding(input, db) {
|
|
67242
|
+
const d = db || getDatabase();
|
|
67243
|
+
assertTask(input.task_id, d);
|
|
67244
|
+
const timestamp3 = input.observed_at || now();
|
|
67245
|
+
const warnings = [];
|
|
67246
|
+
const next = nextFinding(input, d);
|
|
67247
|
+
const existing = getFindingByFingerprint(input.task_id, next.fingerprint, d);
|
|
67248
|
+
const dryRun = !input.apply;
|
|
67249
|
+
if (dryRun) {
|
|
67250
|
+
const action2 = existing ? upsertAction(existing, next) : "preview";
|
|
67251
|
+
return {
|
|
67252
|
+
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
67253
|
+
local_only: true,
|
|
67254
|
+
dry_run: true,
|
|
67255
|
+
processed_at: timestamp3,
|
|
67256
|
+
action: action2,
|
|
67257
|
+
fingerprint: next.fingerprint,
|
|
67258
|
+
finding: existing ? compactFinding(previewFinding(existing, next, timestamp3)) : null,
|
|
67259
|
+
warnings
|
|
67260
|
+
};
|
|
67261
|
+
}
|
|
67262
|
+
if (!existing) {
|
|
67263
|
+
const id = uuid();
|
|
67264
|
+
d.run(`INSERT INTO task_findings (
|
|
67265
|
+
id, task_id, run_id, fingerprint, title, severity, status, source, summary, artifact_path,
|
|
67266
|
+
metadata, first_seen_at, last_seen_at, resolved_at, created_at, updated_at
|
|
67267
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
67268
|
+
id,
|
|
67269
|
+
input.task_id,
|
|
67270
|
+
next.run_id,
|
|
67271
|
+
next.fingerprint,
|
|
67272
|
+
next.title,
|
|
67273
|
+
next.severity,
|
|
67274
|
+
next.status,
|
|
67275
|
+
next.source,
|
|
67276
|
+
next.summary,
|
|
67277
|
+
next.artifact_path,
|
|
67278
|
+
JSON.stringify(next.metadata),
|
|
67279
|
+
timestamp3,
|
|
67280
|
+
timestamp3,
|
|
67281
|
+
next.status === "open" ? null : timestamp3,
|
|
67282
|
+
timestamp3,
|
|
67283
|
+
timestamp3
|
|
67284
|
+
]);
|
|
67285
|
+
return {
|
|
67286
|
+
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
67287
|
+
local_only: true,
|
|
67288
|
+
dry_run: false,
|
|
67289
|
+
processed_at: timestamp3,
|
|
67290
|
+
action: "created",
|
|
67291
|
+
fingerprint: next.fingerprint,
|
|
67292
|
+
finding: compactFinding(getFindingByFingerprint(input.task_id, next.fingerprint, d)),
|
|
67293
|
+
warnings
|
|
67294
|
+
};
|
|
67295
|
+
}
|
|
67296
|
+
if (sameFinding(existing, next)) {
|
|
67297
|
+
return {
|
|
67298
|
+
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
67299
|
+
local_only: true,
|
|
67300
|
+
dry_run: false,
|
|
67301
|
+
processed_at: timestamp3,
|
|
67302
|
+
action: "matched",
|
|
67303
|
+
fingerprint: next.fingerprint,
|
|
67304
|
+
finding: compactFinding(existing),
|
|
67305
|
+
warnings
|
|
67306
|
+
};
|
|
67307
|
+
}
|
|
67308
|
+
const action = upsertAction(existing, next);
|
|
67309
|
+
d.run(`UPDATE task_findings SET
|
|
67310
|
+
run_id = ?, title = ?, severity = ?, status = ?, source = ?, summary = ?, artifact_path = ?,
|
|
67311
|
+
metadata = ?, last_seen_at = ?, resolved_at = ?, updated_at = ?
|
|
67312
|
+
WHERE id = ?`, [
|
|
67313
|
+
next.run_id,
|
|
67314
|
+
next.title,
|
|
67315
|
+
next.severity,
|
|
67316
|
+
next.status,
|
|
67317
|
+
next.source,
|
|
67318
|
+
next.summary,
|
|
67319
|
+
next.artifact_path,
|
|
67320
|
+
JSON.stringify(next.metadata),
|
|
67321
|
+
timestamp3,
|
|
67322
|
+
next.status === "open" ? null : existing.resolved_at || timestamp3,
|
|
67323
|
+
timestamp3,
|
|
67324
|
+
existing.id
|
|
67325
|
+
]);
|
|
67326
|
+
return {
|
|
67327
|
+
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
67328
|
+
local_only: true,
|
|
67329
|
+
dry_run: false,
|
|
67330
|
+
processed_at: timestamp3,
|
|
67331
|
+
action,
|
|
67332
|
+
fingerprint: next.fingerprint,
|
|
67333
|
+
finding: compactFinding(getFindingByFingerprint(input.task_id, next.fingerprint, d)),
|
|
67334
|
+
warnings
|
|
67335
|
+
};
|
|
67336
|
+
}
|
|
67337
|
+
function listTaskFindings(filter = {}, db) {
|
|
67338
|
+
const d = db || getDatabase();
|
|
67339
|
+
const conditions = ["1=1"];
|
|
67340
|
+
const params = [];
|
|
67341
|
+
if (filter.task_id) {
|
|
67342
|
+
conditions.push("task_id = ?");
|
|
67343
|
+
params.push(filter.task_id);
|
|
67344
|
+
}
|
|
67345
|
+
if (filter.run_id) {
|
|
67346
|
+
conditions.push("run_id = ?");
|
|
67347
|
+
params.push(resolveTaskRunId(filter.run_id, d));
|
|
67348
|
+
}
|
|
67349
|
+
if (filter.status) {
|
|
67350
|
+
conditions.push("status = ?");
|
|
67351
|
+
params.push(normalizeStatus(filter.status));
|
|
67352
|
+
}
|
|
67353
|
+
if (filter.source) {
|
|
67354
|
+
conditions.push("source = ?");
|
|
67355
|
+
params.push(redactOptional(filter.source, 120));
|
|
67356
|
+
}
|
|
67357
|
+
const limit = Math.min(Math.max(Math.floor(filter.limit ?? 50), 1), 500);
|
|
67358
|
+
const rows = d.query(`SELECT * FROM task_findings WHERE ${conditions.join(" AND ")} ORDER BY updated_at DESC, created_at DESC LIMIT ?`).all(...[...params, limit]);
|
|
67359
|
+
return rows.map(rowToFinding);
|
|
67360
|
+
}
|
|
67361
|
+
function listCompactTaskFindings(filter = {}, db) {
|
|
67362
|
+
return listTaskFindings(filter, db).map(compactFinding);
|
|
67363
|
+
}
|
|
67364
|
+
function resolveMissingTaskFindings(input, db) {
|
|
67365
|
+
const d = db || getDatabase();
|
|
67366
|
+
assertTask(input.task_id, d);
|
|
67367
|
+
const timestamp3 = input.resolved_at || now();
|
|
67368
|
+
const status = normalizeResolutionStatus(input.status);
|
|
67369
|
+
const runId = resolveRunForTask(input.run_id, input.task_id, d);
|
|
67370
|
+
const present = new Set(input.fingerprints.map(normalizeFingerprint));
|
|
67371
|
+
const warnings = [];
|
|
67372
|
+
const conditions = ["task_id = ?", "status = 'open'"];
|
|
67373
|
+
const params = [input.task_id];
|
|
67374
|
+
if (input.source) {
|
|
67375
|
+
conditions.push("source = ?");
|
|
67376
|
+
params.push(redactOptional(input.source, 120));
|
|
67377
|
+
}
|
|
67378
|
+
const candidates = d.query(`SELECT * FROM task_findings WHERE ${conditions.join(" AND ")} ORDER BY updated_at DESC, created_at DESC`).all(...params).map(rowToFinding).filter((finding2) => !present.has(finding2.fingerprint));
|
|
67379
|
+
const limit = Math.min(Math.max(Math.floor(input.limit ?? 50), 1), 200);
|
|
67380
|
+
const display = candidates.slice(0, limit);
|
|
67381
|
+
const omittedCount = Math.max(0, candidates.length - display.length);
|
|
67382
|
+
if (!input.apply) {
|
|
67383
|
+
return {
|
|
67384
|
+
schema_version: TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
|
|
67385
|
+
local_only: true,
|
|
67386
|
+
dry_run: true,
|
|
67387
|
+
processed_at: timestamp3,
|
|
67388
|
+
action: candidates.length > 0 ? "preview" : "noop",
|
|
67389
|
+
task_id: input.task_id,
|
|
67390
|
+
source: input.source ? redactOptional(input.source, 120) : null,
|
|
67391
|
+
run_id: runId,
|
|
67392
|
+
present_fingerprint_count: present.size,
|
|
67393
|
+
candidate_count: candidates.length,
|
|
67394
|
+
changed_count: 0,
|
|
67395
|
+
omitted_count: omittedCount,
|
|
67396
|
+
findings: display.map(compactFinding),
|
|
67397
|
+
warnings
|
|
67398
|
+
};
|
|
67399
|
+
}
|
|
67400
|
+
const metadataPatch = redactValue({
|
|
67401
|
+
resolved_by: {
|
|
67402
|
+
agent_id: input.agent_id ?? null,
|
|
67403
|
+
run_id: runId,
|
|
67404
|
+
reason: input.reason ? redactEvidenceText(input.reason) : "missing from latest loop finding set"
|
|
67405
|
+
}
|
|
67406
|
+
});
|
|
67407
|
+
const tx = d.transaction(() => {
|
|
67408
|
+
for (const finding2 of candidates) {
|
|
67409
|
+
d.run("UPDATE task_findings SET status = ?, resolved_at = ?, metadata = ?, updated_at = ? WHERE id = ? AND status = 'open'", [
|
|
67410
|
+
status,
|
|
67411
|
+
timestamp3,
|
|
67412
|
+
JSON.stringify({ ...finding2.metadata, ...metadataPatch }),
|
|
67413
|
+
timestamp3,
|
|
67414
|
+
finding2.id
|
|
67415
|
+
]);
|
|
67416
|
+
}
|
|
67417
|
+
});
|
|
67418
|
+
tx();
|
|
67419
|
+
const updated = candidates.map((finding2) => d.query("SELECT * FROM task_findings WHERE id = ?").get(finding2.id)).filter((row) => Boolean(row)).map(rowToFinding);
|
|
67420
|
+
const visibleUpdated = updated.slice(0, limit);
|
|
67421
|
+
return {
|
|
67422
|
+
schema_version: TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
|
|
67423
|
+
local_only: true,
|
|
67424
|
+
dry_run: false,
|
|
67425
|
+
processed_at: timestamp3,
|
|
67426
|
+
action: updated.length > 0 ? status : "noop",
|
|
67427
|
+
task_id: input.task_id,
|
|
67428
|
+
source: input.source ? redactOptional(input.source, 120) : null,
|
|
67429
|
+
run_id: runId,
|
|
67430
|
+
present_fingerprint_count: present.size,
|
|
67431
|
+
candidate_count: candidates.length,
|
|
67432
|
+
changed_count: updated.length,
|
|
67433
|
+
omitted_count: omittedCount,
|
|
67434
|
+
findings: visibleUpdated.map(compactFinding),
|
|
67435
|
+
warnings
|
|
67436
|
+
};
|
|
67437
|
+
}
|
|
67438
|
+
var TASK_FINDING_SCHEMA_VERSION = "todos.task_finding.v1", TASK_FINDING_UPSERT_SCHEMA_VERSION = "todos.task_finding_upsert.v1", TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION = "todos.task_finding_resolve_missing.v1", SEVERITIES, STATUSES;
|
|
67439
|
+
var init_findings = __esm(() => {
|
|
67440
|
+
init_redaction();
|
|
67441
|
+
init_types();
|
|
67442
|
+
init_database();
|
|
67443
|
+
init_tasks();
|
|
67444
|
+
init_task_runs();
|
|
67445
|
+
SEVERITIES = new Set(["low", "medium", "high", "critical"]);
|
|
67446
|
+
STATUSES = new Set(["open", "resolved", "ignored"]);
|
|
67447
|
+
});
|
|
67448
|
+
|
|
66519
67449
|
// src/lib/agent-run-dispatcher.ts
|
|
66520
67450
|
function dispatcherFromRun(run) {
|
|
66521
67451
|
const value = run.metadata["agent_run_dispatcher"];
|
|
@@ -67176,7 +68106,7 @@ function parseArray2(value) {
|
|
|
67176
68106
|
return [];
|
|
67177
68107
|
}
|
|
67178
68108
|
}
|
|
67179
|
-
function
|
|
68109
|
+
function parseObject3(value) {
|
|
67180
68110
|
try {
|
|
67181
68111
|
const parsed = JSON.parse(value || "{}");
|
|
67182
68112
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
@@ -67217,7 +68147,7 @@ function rowToKnowledgeRecord(row) {
|
|
|
67217
68147
|
agent_id: row.agent_id,
|
|
67218
68148
|
snapshot_id: row.snapshot_id,
|
|
67219
68149
|
tags: parseArray2(row.tags),
|
|
67220
|
-
metadata: redactValue(
|
|
68150
|
+
metadata: redactValue(parseObject3(row.metadata)),
|
|
67221
68151
|
created_at: row.created_at,
|
|
67222
68152
|
updated_at: row.updated_at
|
|
67223
68153
|
};
|
|
@@ -67436,7 +68366,7 @@ function parseArray3(value) {
|
|
|
67436
68366
|
return [];
|
|
67437
68367
|
}
|
|
67438
68368
|
}
|
|
67439
|
-
function
|
|
68369
|
+
function parseObject4(value) {
|
|
67440
68370
|
try {
|
|
67441
68371
|
const parsed = JSON.parse(value || "{}");
|
|
67442
68372
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
@@ -67485,7 +68415,7 @@ function rowToRisk(row) {
|
|
|
67485
68415
|
plan_id: row.plan_id,
|
|
67486
68416
|
task_id: row.task_id,
|
|
67487
68417
|
tags: parseArray3(row.tags),
|
|
67488
|
-
metadata: redactValue(
|
|
68418
|
+
metadata: redactValue(parseObject4(row.metadata)),
|
|
67489
68419
|
created_at: row.created_at,
|
|
67490
68420
|
updated_at: row.updated_at,
|
|
67491
68421
|
closed_at: row.closed_at
|
|
@@ -73303,6 +74233,38 @@ ${lines.join(`
|
|
|
73303
74233
|
}
|
|
73304
74234
|
});
|
|
73305
74235
|
}
|
|
74236
|
+
if (shouldRegisterTool("begin_task_run_transaction")) {
|
|
74237
|
+
server.tool("begin_task_run_transaction", "Preview or apply an idempotent local loop run transaction keyed by a stable loop/run id.", {
|
|
74238
|
+
task_id: exports_external.string().describe("Task ID"),
|
|
74239
|
+
key: exports_external.string().optional().describe("Stable idempotency key"),
|
|
74240
|
+
loop_id: exports_external.string().optional().describe("Loop identifier; used as key fallback"),
|
|
74241
|
+
loop_run_id: exports_external.string().optional().describe("Loop run identifier; used as key fallback"),
|
|
74242
|
+
agent_id: exports_external.string().optional().describe("Agent starting the run"),
|
|
74243
|
+
title: exports_external.string().optional().describe("Run title"),
|
|
74244
|
+
summary: exports_external.string().optional().describe("Run summary"),
|
|
74245
|
+
metadata: exports_external.record(exports_external.unknown()).optional().describe("Additional local metadata"),
|
|
74246
|
+
claim: exports_external.boolean().optional().describe("Claim/start the task before recording the run"),
|
|
74247
|
+
apply: exports_external.boolean().optional().describe("Apply the transaction; false or omitted is dry-run")
|
|
74248
|
+
}, async ({ task_id, key, loop_id, loop_run_id, agent_id, title, summary, metadata, claim, apply }) => {
|
|
74249
|
+
try {
|
|
74250
|
+
const result = beginTaskRunTransaction({
|
|
74251
|
+
task_id: resolveId(task_id),
|
|
74252
|
+
key,
|
|
74253
|
+
loop_id,
|
|
74254
|
+
loop_run_id,
|
|
74255
|
+
agent_id,
|
|
74256
|
+
title,
|
|
74257
|
+
summary,
|
|
74258
|
+
metadata,
|
|
74259
|
+
claim,
|
|
74260
|
+
apply
|
|
74261
|
+
});
|
|
74262
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
74263
|
+
} catch (e) {
|
|
74264
|
+
return { content: [{ type: "text", text: formatError2(e) }], isError: true };
|
|
74265
|
+
}
|
|
74266
|
+
});
|
|
74267
|
+
}
|
|
73306
74268
|
if (shouldRegisterTool("list_task_runs")) {
|
|
73307
74269
|
server.tool("list_task_runs", "List local run ledger entries, optionally scoped to a task.", { task_id: exports_external.string().optional().describe("Optional task ID") }, async ({ task_id }) => {
|
|
73308
74270
|
try {
|
|
@@ -73400,15 +74362,117 @@ ${lines.join(`
|
|
|
73400
74362
|
});
|
|
73401
74363
|
}
|
|
73402
74364
|
if (shouldRegisterTool("finish_task_run")) {
|
|
73403
|
-
server.tool("finish_task_run", "Finish a local run ledger entry as completed, failed, or cancelled.", {
|
|
73404
|
-
run_id: exports_external.string().describe("Run ID or prefix"),
|
|
73405
|
-
|
|
74365
|
+
server.tool("finish_task_run", "Finish a local run ledger entry as completed, failed, or cancelled. Supports idempotent lookup by key.", {
|
|
74366
|
+
run_id: exports_external.string().optional().describe("Run ID or prefix"),
|
|
74367
|
+
key: exports_external.string().optional().describe("Idempotency key when run_id is omitted"),
|
|
74368
|
+
task_id: exports_external.string().optional().describe("Task scope for key lookup"),
|
|
74369
|
+
status: exports_external.enum(["completed", "failed", "cancelled"]).optional().describe("Final run status"),
|
|
73406
74370
|
summary: exports_external.string().optional().describe("Final summary"),
|
|
73407
|
-
agent_id: exports_external.string().optional().describe("Agent finishing the run")
|
|
73408
|
-
|
|
74371
|
+
agent_id: exports_external.string().optional().describe("Agent finishing the run"),
|
|
74372
|
+
apply: exports_external.boolean().optional().describe("Apply the transaction; false previews without mutating")
|
|
74373
|
+
}, async ({ run_id, key, task_id, status, summary, agent_id, apply }) => {
|
|
73409
74374
|
try {
|
|
73410
|
-
|
|
73411
|
-
|
|
74375
|
+
if (run_id && !key && apply === undefined) {
|
|
74376
|
+
const result2 = finishTaskRunTransaction({ run_id, status: status || "completed", summary, agent_id, apply: true });
|
|
74377
|
+
const run = result2.run ? getTaskRunLedger(result2.run.id).run : null;
|
|
74378
|
+
return { content: [{ type: "text", text: JSON.stringify(run, null, 2) }] };
|
|
74379
|
+
}
|
|
74380
|
+
const result = finishTaskRunTransaction({
|
|
74381
|
+
run_id,
|
|
74382
|
+
key,
|
|
74383
|
+
task_id: task_id ? resolveId(task_id) : undefined,
|
|
74384
|
+
status: status || "completed",
|
|
74385
|
+
summary,
|
|
74386
|
+
agent_id,
|
|
74387
|
+
apply: apply !== false
|
|
74388
|
+
});
|
|
74389
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
74390
|
+
} catch (e) {
|
|
74391
|
+
return { content: [{ type: "text", text: formatError2(e) }], isError: true };
|
|
74392
|
+
}
|
|
74393
|
+
});
|
|
74394
|
+
}
|
|
74395
|
+
if (shouldRegisterTool("upsert_task_finding")) {
|
|
74396
|
+
server.tool("upsert_task_finding", "Preview or apply an idempotent local finding upsert scoped by task and fingerprint.", {
|
|
74397
|
+
task_id: exports_external.string().describe("Task ID"),
|
|
74398
|
+
fingerprint: exports_external.string().describe("Stable finding fingerprint"),
|
|
74399
|
+
title: exports_external.string().describe("Finding title"),
|
|
74400
|
+
severity: exports_external.enum(["low", "medium", "high", "critical"]).optional().describe("Finding severity"),
|
|
74401
|
+
status: exports_external.enum(["open", "resolved", "ignored"]).optional().describe("Finding status"),
|
|
74402
|
+
source: exports_external.string().optional().describe("Loop/tool source name"),
|
|
74403
|
+
summary: exports_external.string().optional().describe("Bounded finding summary"),
|
|
74404
|
+
artifact_path: exports_external.string().optional().describe("Local artifact path/reference; content is not read"),
|
|
74405
|
+
run_id: exports_external.string().optional().describe("Run ledger ID or prefix"),
|
|
74406
|
+
metadata: exports_external.record(exports_external.unknown()).optional().describe("Additional local metadata"),
|
|
74407
|
+
apply: exports_external.boolean().optional().describe("Apply the upsert; false or omitted is dry-run")
|
|
74408
|
+
}, async ({ task_id, fingerprint: fingerprint3, title, severity, status, source: source3, summary, artifact_path, run_id, metadata, apply }) => {
|
|
74409
|
+
try {
|
|
74410
|
+
const result = upsertTaskFinding({
|
|
74411
|
+
task_id: resolveId(task_id),
|
|
74412
|
+
fingerprint: fingerprint3,
|
|
74413
|
+
title,
|
|
74414
|
+
severity,
|
|
74415
|
+
status,
|
|
74416
|
+
source: source3,
|
|
74417
|
+
summary,
|
|
74418
|
+
artifact_path,
|
|
74419
|
+
run_id,
|
|
74420
|
+
metadata,
|
|
74421
|
+
apply
|
|
74422
|
+
});
|
|
74423
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
74424
|
+
} catch (e) {
|
|
74425
|
+
return { content: [{ type: "text", text: formatError2(e) }], isError: true };
|
|
74426
|
+
}
|
|
74427
|
+
});
|
|
74428
|
+
}
|
|
74429
|
+
if (shouldRegisterTool("list_task_findings")) {
|
|
74430
|
+
server.tool("list_task_findings", "List compact local findings with bounded output.", {
|
|
74431
|
+
task_id: exports_external.string().optional().describe("Filter by task"),
|
|
74432
|
+
run_id: exports_external.string().optional().describe("Filter by run"),
|
|
74433
|
+
status: exports_external.enum(["open", "resolved", "ignored"]).optional().describe("Filter by status"),
|
|
74434
|
+
source: exports_external.string().optional().describe("Filter by source"),
|
|
74435
|
+
limit: exports_external.number().optional().describe("Maximum findings to return")
|
|
74436
|
+
}, async ({ task_id, run_id, status, source: source3, limit }) => {
|
|
74437
|
+
try {
|
|
74438
|
+
const findings = listCompactTaskFindings({
|
|
74439
|
+
task_id: task_id ? resolveId(task_id) : undefined,
|
|
74440
|
+
run_id,
|
|
74441
|
+
status,
|
|
74442
|
+
source: source3,
|
|
74443
|
+
limit
|
|
74444
|
+
});
|
|
74445
|
+
return { content: [{ type: "text", text: JSON.stringify(findings) }] };
|
|
74446
|
+
} catch (e) {
|
|
74447
|
+
return { content: [{ type: "text", text: formatError2(e) }], isError: true };
|
|
74448
|
+
}
|
|
74449
|
+
});
|
|
74450
|
+
}
|
|
74451
|
+
if (shouldRegisterTool("resolve_missing_task_findings")) {
|
|
74452
|
+
server.tool("resolve_missing_task_findings", "Resolve open findings absent from the latest loop finding set.", {
|
|
74453
|
+
task_id: exports_external.string().describe("Task ID"),
|
|
74454
|
+
fingerprints: exports_external.array(exports_external.string()).optional().describe("Fingerprints still present in the latest run"),
|
|
74455
|
+
source: exports_external.string().optional().describe("Only resolve findings from this source"),
|
|
74456
|
+
run_id: exports_external.string().optional().describe("Run ledger ID or prefix for audit metadata"),
|
|
74457
|
+
status: exports_external.enum(["resolved", "ignored"]).optional().describe("Resolution status"),
|
|
74458
|
+
agent_id: exports_external.string().optional().describe("Agent resolving findings"),
|
|
74459
|
+
reason: exports_external.string().optional().describe("Resolution reason"),
|
|
74460
|
+
limit: exports_external.number().optional().describe("Maximum findings returned"),
|
|
74461
|
+
apply: exports_external.boolean().optional().describe("Apply resolution; false or omitted is dry-run")
|
|
74462
|
+
}, async ({ task_id, fingerprints, source: source3, run_id, status, agent_id, reason, limit, apply }) => {
|
|
74463
|
+
try {
|
|
74464
|
+
const result = resolveMissingTaskFindings({
|
|
74465
|
+
task_id: resolveId(task_id),
|
|
74466
|
+
fingerprints: fingerprints || [],
|
|
74467
|
+
source: source3,
|
|
74468
|
+
run_id,
|
|
74469
|
+
status,
|
|
74470
|
+
agent_id,
|
|
74471
|
+
reason,
|
|
74472
|
+
limit,
|
|
74473
|
+
apply
|
|
74474
|
+
});
|
|
74475
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
73412
74476
|
} catch (e) {
|
|
73413
74477
|
return { content: [{ type: "text", text: formatError2(e) }], isError: true };
|
|
73414
74478
|
}
|
|
@@ -73744,6 +74808,7 @@ var init_task_resources = __esm(() => {
|
|
|
73744
74808
|
init_agents();
|
|
73745
74809
|
init_task_commits();
|
|
73746
74810
|
init_task_runs();
|
|
74811
|
+
init_findings();
|
|
73747
74812
|
init_agent_run_dispatcher();
|
|
73748
74813
|
init_verification_providers();
|
|
73749
74814
|
init_release_notes();
|
|
@@ -78319,8 +79384,10 @@ function taskToSummary(task2, fields) {
|
|
|
78319
79384
|
task_list_id: task2.task_list_id,
|
|
78320
79385
|
agent_id: task2.agent_id,
|
|
78321
79386
|
assigned_to: task2.assigned_to,
|
|
79387
|
+
working_dir: task2.working_dir,
|
|
78322
79388
|
locked_by: task2.locked_by,
|
|
78323
79389
|
tags: task2.tags,
|
|
79390
|
+
metadata: task2.metadata,
|
|
78324
79391
|
version: task2.version,
|
|
78325
79392
|
created_at: task2.created_at,
|
|
78326
79393
|
updated_at: task2.updated_at,
|
|
@@ -78462,6 +79529,9 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
78462
79529
|
if (path === "/api/tasks" && method === "POST") {
|
|
78463
79530
|
return handleCreateTask(req, ctx, jsonWithCors, taskToSummary);
|
|
78464
79531
|
}
|
|
79532
|
+
if (path === "/api/tasks/upsert" && method === "POST") {
|
|
79533
|
+
return handleUpsertTask(req, ctx, jsonWithCors, taskToSummary);
|
|
79534
|
+
}
|
|
78465
79535
|
if (path === "/api/tasks/export" && method === "GET") {
|
|
78466
79536
|
return handleTasksExport(req, url, ctx, jsonWithCors, taskToSummary);
|
|
78467
79537
|
}
|
|
@@ -78657,7 +79727,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
78657
79727
|
} catch {}
|
|
78658
79728
|
}
|
|
78659
79729
|
}
|
|
78660
|
-
var MIME_TYPES, SECURITY_HEADERS, rateLimitMap, RATE_LIMIT_WINDOW_MS = 60000, RATE_LIMIT_MAX
|
|
79730
|
+
var MIME_TYPES, SECURITY_HEADERS, rateLimitMap, RATE_LIMIT_WINDOW_MS = 60000, RATE_LIMIT_MAX;
|
|
78661
79731
|
var init_serve = __esm(() => {
|
|
78662
79732
|
init_database();
|
|
78663
79733
|
init_api_keys();
|
|
@@ -78682,6 +79752,7 @@ var init_serve = __esm(() => {
|
|
|
78682
79752
|
"Permissions-Policy": "camera=, microphone=, geolocation="
|
|
78683
79753
|
};
|
|
78684
79754
|
rateLimitMap = new Map;
|
|
79755
|
+
RATE_LIMIT_MAX = Number.parseInt(process.env["TODOS_RATE_LIMIT_MAX"] || "120", 10);
|
|
78685
79756
|
});
|
|
78686
79757
|
|
|
78687
79758
|
// src/server/index.ts
|