@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/mcp/index.js
CHANGED
|
@@ -1140,6 +1140,49 @@ var init_migrations = __esm(() => {
|
|
|
1140
1140
|
CREATE INDEX IF NOT EXISTS idx_local_retrospectives_plan ON local_retrospectives(plan_id);
|
|
1141
1141
|
CREATE INDEX IF NOT EXISTS idx_local_retrospectives_agent ON local_retrospectives(agent_id);
|
|
1142
1142
|
INSERT OR IGNORE INTO _migrations (id) VALUES (61);
|
|
1143
|
+
`,
|
|
1144
|
+
`
|
|
1145
|
+
CREATE TABLE IF NOT EXISTS task_run_transactions (
|
|
1146
|
+
id TEXT PRIMARY KEY,
|
|
1147
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1148
|
+
run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
|
|
1149
|
+
key TEXT NOT NULL,
|
|
1150
|
+
loop_id TEXT,
|
|
1151
|
+
loop_run_id TEXT,
|
|
1152
|
+
metadata TEXT DEFAULT '{}',
|
|
1153
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1154
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1155
|
+
UNIQUE(task_id, key)
|
|
1156
|
+
);
|
|
1157
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_transactions_task_key ON task_run_transactions(task_id, key);
|
|
1158
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_transactions_key ON task_run_transactions(key);
|
|
1159
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_transactions_run ON task_run_transactions(run_id);
|
|
1160
|
+
|
|
1161
|
+
CREATE TABLE IF NOT EXISTS task_findings (
|
|
1162
|
+
id TEXT PRIMARY KEY,
|
|
1163
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1164
|
+
run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
|
|
1165
|
+
fingerprint TEXT NOT NULL,
|
|
1166
|
+
title TEXT NOT NULL,
|
|
1167
|
+
severity TEXT NOT NULL DEFAULT 'medium' CHECK(severity IN ('low', 'medium', 'high', 'critical')),
|
|
1168
|
+
status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open', 'resolved', 'ignored')),
|
|
1169
|
+
source TEXT,
|
|
1170
|
+
summary TEXT,
|
|
1171
|
+
artifact_path TEXT,
|
|
1172
|
+
metadata TEXT DEFAULT '{}',
|
|
1173
|
+
first_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1174
|
+
last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1175
|
+
resolved_at TEXT,
|
|
1176
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1177
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1178
|
+
UNIQUE(task_id, fingerprint)
|
|
1179
|
+
);
|
|
1180
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_task ON task_findings(task_id);
|
|
1181
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_run ON task_findings(run_id);
|
|
1182
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_status ON task_findings(status);
|
|
1183
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_source ON task_findings(source);
|
|
1184
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_fingerprint ON task_findings(fingerprint);
|
|
1185
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (62);
|
|
1143
1186
|
`
|
|
1144
1187
|
];
|
|
1145
1188
|
});
|
|
@@ -1577,6 +1620,47 @@ function ensureSchema(db) {
|
|
|
1577
1620
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_run ON task_run_artifacts(run_id)");
|
|
1578
1621
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_task ON task_run_artifacts(task_id)");
|
|
1579
1622
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_path ON task_run_artifacts(path)");
|
|
1623
|
+
ensureTable("task_run_transactions", `
|
|
1624
|
+
CREATE TABLE task_run_transactions (
|
|
1625
|
+
id TEXT PRIMARY KEY,
|
|
1626
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1627
|
+
run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
|
|
1628
|
+
key TEXT NOT NULL,
|
|
1629
|
+
loop_id TEXT,
|
|
1630
|
+
loop_run_id TEXT,
|
|
1631
|
+
metadata TEXT DEFAULT '{}',
|
|
1632
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1633
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1634
|
+
UNIQUE(task_id, key)
|
|
1635
|
+
)`);
|
|
1636
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_transactions_task_key ON task_run_transactions(task_id, key)");
|
|
1637
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_transactions_key ON task_run_transactions(key)");
|
|
1638
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_transactions_run ON task_run_transactions(run_id)");
|
|
1639
|
+
ensureTable("task_findings", `
|
|
1640
|
+
CREATE TABLE task_findings (
|
|
1641
|
+
id TEXT PRIMARY KEY,
|
|
1642
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1643
|
+
run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
|
|
1644
|
+
fingerprint TEXT NOT NULL,
|
|
1645
|
+
title TEXT NOT NULL,
|
|
1646
|
+
severity TEXT NOT NULL DEFAULT 'medium' CHECK(severity IN ('low', 'medium', 'high', 'critical')),
|
|
1647
|
+
status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open', 'resolved', 'ignored')),
|
|
1648
|
+
source TEXT,
|
|
1649
|
+
summary TEXT,
|
|
1650
|
+
artifact_path TEXT,
|
|
1651
|
+
metadata TEXT DEFAULT '{}',
|
|
1652
|
+
first_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1653
|
+
last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1654
|
+
resolved_at TEXT,
|
|
1655
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1656
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1657
|
+
UNIQUE(task_id, fingerprint)
|
|
1658
|
+
)`);
|
|
1659
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_task ON task_findings(task_id)");
|
|
1660
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_run ON task_findings(run_id)");
|
|
1661
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_status ON task_findings(status)");
|
|
1662
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_source ON task_findings(source)");
|
|
1663
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_fingerprint ON task_findings(fingerprint)");
|
|
1580
1664
|
ensureTable("inbox_items", `
|
|
1581
1665
|
CREATE TABLE inbox_items (
|
|
1582
1666
|
id TEXT PRIMARY KEY,
|
|
@@ -8633,6 +8717,7 @@ var init_event_hooks = __esm(() => {
|
|
|
8633
8717
|
"task.blocked",
|
|
8634
8718
|
"task.started",
|
|
8635
8719
|
"task.completed",
|
|
8720
|
+
"task.updated",
|
|
8636
8721
|
"task.due",
|
|
8637
8722
|
"task.due_soon",
|
|
8638
8723
|
"task.failed",
|
|
@@ -8653,7 +8738,7 @@ var init_event_hooks = __esm(() => {
|
|
|
8653
8738
|
VALID_TARGETS = new Set(["stdout", "file", "socket", "script"]);
|
|
8654
8739
|
});
|
|
8655
8740
|
|
|
8656
|
-
// node_modules/.bun/@hasna+events@0.1.
|
|
8741
|
+
// node_modules/.bun/@hasna+events@0.1.9/node_modules/@hasna/events/dist/index.js
|
|
8657
8742
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
8658
8743
|
import { existsSync as existsSync5 } from "fs";
|
|
8659
8744
|
import { homedir } from "os";
|
|
@@ -8670,17 +8755,30 @@ function getPathValue(input, path) {
|
|
|
8670
8755
|
return;
|
|
8671
8756
|
}, input);
|
|
8672
8757
|
}
|
|
8673
|
-
function wildcardToRegExp(pattern) {
|
|
8674
|
-
|
|
8675
|
-
|
|
8758
|
+
function wildcardToRegExp(pattern, options = {}) {
|
|
8759
|
+
let body = "";
|
|
8760
|
+
for (let index = 0;index < pattern.length; index += 1) {
|
|
8761
|
+
const char = pattern[index];
|
|
8762
|
+
if (char === "*") {
|
|
8763
|
+
if (pattern[index + 1] === "*") {
|
|
8764
|
+
body += ".*";
|
|
8765
|
+
index += 1;
|
|
8766
|
+
} else {
|
|
8767
|
+
body += options.segmentSafe ? "[^/]*" : ".*";
|
|
8768
|
+
}
|
|
8769
|
+
} else {
|
|
8770
|
+
body += char.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
8771
|
+
}
|
|
8772
|
+
}
|
|
8773
|
+
return new RegExp(`^${body}$`);
|
|
8676
8774
|
}
|
|
8677
|
-
function matchString(value, matcher) {
|
|
8775
|
+
function matchString(value, matcher, options = {}) {
|
|
8678
8776
|
if (matcher === undefined)
|
|
8679
8777
|
return true;
|
|
8680
8778
|
if (value === undefined)
|
|
8681
8779
|
return false;
|
|
8682
8780
|
const matchers = Array.isArray(matcher) ? matcher : [matcher];
|
|
8683
|
-
return matchers.some((item) => wildcardToRegExp(item).test(value));
|
|
8781
|
+
return matchers.some((item) => wildcardToRegExp(item, options).test(value));
|
|
8684
8782
|
}
|
|
8685
8783
|
function matchRecord(input, matcher) {
|
|
8686
8784
|
if (!matcher)
|
|
@@ -8688,7 +8786,9 @@ function matchRecord(input, matcher) {
|
|
|
8688
8786
|
return Object.entries(matcher).every(([path, expected]) => {
|
|
8689
8787
|
const actual = getPathValue(input, path);
|
|
8690
8788
|
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
8691
|
-
return matchString(actual === undefined ? undefined : String(actual), expected
|
|
8789
|
+
return matchString(actual === undefined ? undefined : String(actual), expected, {
|
|
8790
|
+
segmentSafe: path.endsWith("_path") || path.endsWith(".path")
|
|
8791
|
+
});
|
|
8692
8792
|
}
|
|
8693
8793
|
return actual === expected;
|
|
8694
8794
|
});
|
|
@@ -9042,7 +9142,7 @@ class EventsClient {
|
|
|
9042
9142
|
}
|
|
9043
9143
|
return deliveries;
|
|
9044
9144
|
}
|
|
9045
|
-
async
|
|
9145
|
+
async matchChannel(id, input = {}) {
|
|
9046
9146
|
const channel = await this.store.getChannel(id);
|
|
9047
9147
|
if (!channel)
|
|
9048
9148
|
throw new Error(`Channel not found: ${id}`);
|
|
@@ -9059,6 +9159,34 @@ class EventsClient {
|
|
|
9059
9159
|
time: input.time,
|
|
9060
9160
|
id: input.id
|
|
9061
9161
|
});
|
|
9162
|
+
const matched = channelMatchesEvent(channel, event);
|
|
9163
|
+
return {
|
|
9164
|
+
channelId: channel.id,
|
|
9165
|
+
matched,
|
|
9166
|
+
event,
|
|
9167
|
+
filters: channel.filters,
|
|
9168
|
+
reason: matched ? undefined : channel.enabled ? "event did not match channel filters" : "channel is disabled"
|
|
9169
|
+
};
|
|
9170
|
+
}
|
|
9171
|
+
async testChannel(id, input = {}, options = {}) {
|
|
9172
|
+
const channel = await this.store.getChannel(id);
|
|
9173
|
+
if (!channel)
|
|
9174
|
+
throw new Error(`Channel not found: ${id}`);
|
|
9175
|
+
const match = await this.matchChannel(id, input);
|
|
9176
|
+
const event = match.event;
|
|
9177
|
+
if (options.honorFilters && !match.matched) {
|
|
9178
|
+
const timestamp = new Date().toISOString();
|
|
9179
|
+
const result2 = createDeliveryResult(event, channel, [{
|
|
9180
|
+
attempt: 1,
|
|
9181
|
+
status: "skipped",
|
|
9182
|
+
startedAt: timestamp,
|
|
9183
|
+
completedAt: timestamp,
|
|
9184
|
+
error: match.reason
|
|
9185
|
+
}]);
|
|
9186
|
+
result2.metadata = { reason: "filter_mismatch" };
|
|
9187
|
+
await this.store.appendDelivery(result2);
|
|
9188
|
+
return result2;
|
|
9189
|
+
}
|
|
9062
9190
|
const eventForChannel = await this.applyRedaction(event, channel);
|
|
9063
9191
|
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
9064
9192
|
await this.store.appendDelivery(result);
|
|
@@ -9162,6 +9290,90 @@ var init_dist = __esm(() => {
|
|
|
9162
9290
|
DEFAULT_SIGNATURE_TOLERANCE_MS = 5 * 60 * 1000;
|
|
9163
9291
|
});
|
|
9164
9292
|
|
|
9293
|
+
// src/db/task-lists.ts
|
|
9294
|
+
function rowToTaskList(row) {
|
|
9295
|
+
return {
|
|
9296
|
+
...row,
|
|
9297
|
+
metadata: JSON.parse(row.metadata || "{}")
|
|
9298
|
+
};
|
|
9299
|
+
}
|
|
9300
|
+
function createTaskList(input, db) {
|
|
9301
|
+
const d = db || getDatabase();
|
|
9302
|
+
const id = uuid();
|
|
9303
|
+
const timestamp = now();
|
|
9304
|
+
const slug = input.slug || slugify(input.name);
|
|
9305
|
+
if (!input.project_id) {
|
|
9306
|
+
const existing = d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ?").get(slug);
|
|
9307
|
+
if (existing) {
|
|
9308
|
+
throw new Error(`Standalone task list with slug "${slug}" already exists`);
|
|
9309
|
+
}
|
|
9310
|
+
}
|
|
9311
|
+
d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at)
|
|
9312
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp]);
|
|
9313
|
+
return getTaskList(id, d);
|
|
9314
|
+
}
|
|
9315
|
+
function getTaskList(id, db) {
|
|
9316
|
+
const d = db || getDatabase();
|
|
9317
|
+
const row = d.query("SELECT * FROM task_lists WHERE id = ?").get(id);
|
|
9318
|
+
return row ? rowToTaskList(row) : null;
|
|
9319
|
+
}
|
|
9320
|
+
function getTaskListBySlug(slug, projectId, db) {
|
|
9321
|
+
const d = db || getDatabase();
|
|
9322
|
+
let row;
|
|
9323
|
+
if (projectId) {
|
|
9324
|
+
row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id = ?").get(slug, projectId);
|
|
9325
|
+
} else {
|
|
9326
|
+
row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id IS NULL").get(slug);
|
|
9327
|
+
}
|
|
9328
|
+
return row ? rowToTaskList(row) : null;
|
|
9329
|
+
}
|
|
9330
|
+
function listTaskLists(projectId, db) {
|
|
9331
|
+
const d = db || getDatabase();
|
|
9332
|
+
if (projectId) {
|
|
9333
|
+
return d.query("SELECT * FROM task_lists WHERE project_id = ? ORDER BY name").all(projectId).map(rowToTaskList);
|
|
9334
|
+
}
|
|
9335
|
+
return d.query("SELECT * FROM task_lists ORDER BY name").all().map(rowToTaskList);
|
|
9336
|
+
}
|
|
9337
|
+
function updateTaskList(id, input, db) {
|
|
9338
|
+
const d = db || getDatabase();
|
|
9339
|
+
const existing = getTaskList(id, d);
|
|
9340
|
+
if (!existing)
|
|
9341
|
+
throw new TaskListNotFoundError(id);
|
|
9342
|
+
const sets = ["updated_at = ?"];
|
|
9343
|
+
const params = [now()];
|
|
9344
|
+
if (input.name !== undefined) {
|
|
9345
|
+
sets.push("name = ?");
|
|
9346
|
+
params.push(input.name);
|
|
9347
|
+
}
|
|
9348
|
+
if (input.description !== undefined) {
|
|
9349
|
+
sets.push("description = ?");
|
|
9350
|
+
params.push(input.description);
|
|
9351
|
+
}
|
|
9352
|
+
if (input.metadata !== undefined) {
|
|
9353
|
+
sets.push("metadata = ?");
|
|
9354
|
+
params.push(JSON.stringify(input.metadata));
|
|
9355
|
+
}
|
|
9356
|
+
params.push(id);
|
|
9357
|
+
d.run(`UPDATE task_lists SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
9358
|
+
return getTaskList(id, d);
|
|
9359
|
+
}
|
|
9360
|
+
function deleteTaskList(id, db) {
|
|
9361
|
+
const d = db || getDatabase();
|
|
9362
|
+
return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
|
|
9363
|
+
}
|
|
9364
|
+
function ensureTaskList(name, slug, projectId, db) {
|
|
9365
|
+
const d = db || getDatabase();
|
|
9366
|
+
const existing = getTaskListBySlug(slug, projectId, d);
|
|
9367
|
+
if (existing)
|
|
9368
|
+
return existing;
|
|
9369
|
+
return createTaskList({ name, slug, project_id: projectId }, d);
|
|
9370
|
+
}
|
|
9371
|
+
var init_task_lists = __esm(() => {
|
|
9372
|
+
init_types();
|
|
9373
|
+
init_database();
|
|
9374
|
+
init_projects();
|
|
9375
|
+
});
|
|
9376
|
+
|
|
9165
9377
|
// src/lib/shared-events.ts
|
|
9166
9378
|
function taskEventData(task, extra = {}) {
|
|
9167
9379
|
return {
|
|
@@ -9191,6 +9403,69 @@ function taskEventData(task, extra = {}) {
|
|
|
9191
9403
|
...extra
|
|
9192
9404
|
};
|
|
9193
9405
|
}
|
|
9406
|
+
function taskEventMetadata(task) {
|
|
9407
|
+
const metadata = {
|
|
9408
|
+
package: "@hasna/todos",
|
|
9409
|
+
todos_event_schema_version: 1,
|
|
9410
|
+
task_id: task.id,
|
|
9411
|
+
task_short_id: task.short_id,
|
|
9412
|
+
project_id: task.project_id,
|
|
9413
|
+
task_list_id: task.task_list_id,
|
|
9414
|
+
working_dir: task.working_dir
|
|
9415
|
+
};
|
|
9416
|
+
try {
|
|
9417
|
+
const project = task.project_id ? getProject(task.project_id) : null;
|
|
9418
|
+
const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
|
|
9419
|
+
if (project) {
|
|
9420
|
+
metadata.project_id = project.id;
|
|
9421
|
+
metadata.project_name = project.name;
|
|
9422
|
+
metadata.project_path = projectPath;
|
|
9423
|
+
metadata.project_canonical_path = project.path;
|
|
9424
|
+
metadata.project_default_task_list_slug = project.task_list_id;
|
|
9425
|
+
metadata.root_project_id = inferRootProjectId(project);
|
|
9426
|
+
} else if (projectPath) {
|
|
9427
|
+
metadata.project_path = projectPath;
|
|
9428
|
+
metadata.project_canonical_path = projectPath;
|
|
9429
|
+
}
|
|
9430
|
+
if (projectPath) {
|
|
9431
|
+
metadata.project_kind = classifyProjectKind(projectPath);
|
|
9432
|
+
metadata.project_is_worktree = isWorktreePath(projectPath);
|
|
9433
|
+
if (typeof task.metadata.route_enabled === "boolean") {
|
|
9434
|
+
metadata.route_enabled = task.metadata.route_enabled;
|
|
9435
|
+
}
|
|
9436
|
+
metadata.working_dir = task.working_dir ?? projectPath;
|
|
9437
|
+
}
|
|
9438
|
+
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;
|
|
9439
|
+
if (taskList) {
|
|
9440
|
+
metadata.task_list_id = taskList.id;
|
|
9441
|
+
metadata.task_list_slug = taskList.slug;
|
|
9442
|
+
metadata.task_list_name = taskList.name;
|
|
9443
|
+
metadata.task_list_project_id = taskList.project_id;
|
|
9444
|
+
metadata.task_list_is_project_default = Boolean(project?.task_list_id && taskList.slug === project.task_list_id);
|
|
9445
|
+
}
|
|
9446
|
+
} catch {}
|
|
9447
|
+
return metadata;
|
|
9448
|
+
}
|
|
9449
|
+
function classifyProjectKind(path) {
|
|
9450
|
+
return path.includes("/hasna/opensource/") ? "open-source" : "unknown";
|
|
9451
|
+
}
|
|
9452
|
+
function isWorktreePath(path) {
|
|
9453
|
+
return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
|
|
9454
|
+
}
|
|
9455
|
+
function inferRootProjectId(project) {
|
|
9456
|
+
return isWorktreePath(project.path) ? null : project.id;
|
|
9457
|
+
}
|
|
9458
|
+
function readMachineLocalPath(project) {
|
|
9459
|
+
const machineId = process.env["TODOS_MACHINE_ID"];
|
|
9460
|
+
if (!machineId)
|
|
9461
|
+
return null;
|
|
9462
|
+
try {
|
|
9463
|
+
const row = getDatabase().query("SELECT path FROM project_machine_paths WHERE project_id = ? AND machine_id = ?").get(project.id, machineId);
|
|
9464
|
+
return row?.path ?? null;
|
|
9465
|
+
} catch {
|
|
9466
|
+
return null;
|
|
9467
|
+
}
|
|
9468
|
+
}
|
|
9194
9469
|
async function emitSharedTaskEvent(input) {
|
|
9195
9470
|
const data = taskEventData(input.task, input.data);
|
|
9196
9471
|
await new EventsClient().emit({
|
|
@@ -9201,12 +9476,7 @@ async function emitSharedTaskEvent(input) {
|
|
|
9201
9476
|
message: input.message ?? `${input.type}: ${input.task.title}`,
|
|
9202
9477
|
data,
|
|
9203
9478
|
dedupeKey: input.dedupeKey ?? `${input.type}:${input.task.id}:${input.task.version}`,
|
|
9204
|
-
metadata:
|
|
9205
|
-
package: "@hasna/todos",
|
|
9206
|
-
task_id: input.task.id,
|
|
9207
|
-
project_id: input.task.project_id,
|
|
9208
|
-
task_list_id: input.task.task_list_id
|
|
9209
|
-
}
|
|
9479
|
+
metadata: taskEventMetadata(input.task)
|
|
9210
9480
|
}, { deliver: true, dedupe: true });
|
|
9211
9481
|
}
|
|
9212
9482
|
function emitSharedTaskEventQuiet(input) {
|
|
@@ -9217,6 +9487,9 @@ function emitSharedTaskEventQuiet(input) {
|
|
|
9217
9487
|
var SOURCE = "todos";
|
|
9218
9488
|
var init_shared_events = __esm(() => {
|
|
9219
9489
|
init_dist();
|
|
9490
|
+
init_database();
|
|
9491
|
+
init_projects();
|
|
9492
|
+
init_task_lists();
|
|
9220
9493
|
});
|
|
9221
9494
|
|
|
9222
9495
|
// src/lib/secret-redaction.ts
|
|
@@ -9741,6 +10014,17 @@ function replaceTaskTags(taskId, tags, db) {
|
|
|
9741
10014
|
db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
|
|
9742
10015
|
insertTaskTags(taskId, tags, db);
|
|
9743
10016
|
}
|
|
10017
|
+
function addMetadataConditions(metadata, conditions, params) {
|
|
10018
|
+
if (!metadata)
|
|
10019
|
+
return;
|
|
10020
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
10021
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(key)) {
|
|
10022
|
+
throw new Error(`Invalid metadata filter key: ${key}`);
|
|
10023
|
+
}
|
|
10024
|
+
conditions.push(`json_extract(metadata, '$."${key}"') = ?`);
|
|
10025
|
+
params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
|
|
10026
|
+
}
|
|
10027
|
+
}
|
|
9744
10028
|
function createTask(input, db) {
|
|
9745
10029
|
const d = db || getDatabase();
|
|
9746
10030
|
const timestamp = now();
|
|
@@ -9923,6 +10207,7 @@ function listTasks(filter = {}, db) {
|
|
|
9923
10207
|
params.push(filter.task_type);
|
|
9924
10208
|
}
|
|
9925
10209
|
}
|
|
10210
|
+
addMetadataConditions(filter.metadata, conditions, params);
|
|
9926
10211
|
const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
|
|
9927
10212
|
if (filter.cursor) {
|
|
9928
10213
|
try {
|
|
@@ -9947,6 +10232,54 @@ function listTasks(filter = {}, db) {
|
|
|
9947
10232
|
const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC${limitClause}`).all(...params);
|
|
9948
10233
|
return rows.map(rowToTask);
|
|
9949
10234
|
}
|
|
10235
|
+
function getTaskByFingerprint(fingerprint, db) {
|
|
10236
|
+
const tasks = listTasks({ metadata: { fingerprint }, limit: 1 }, db);
|
|
10237
|
+
return tasks[0] ?? null;
|
|
10238
|
+
}
|
|
10239
|
+
function mergeTaskMetadata(current, next, fingerprint) {
|
|
10240
|
+
return {
|
|
10241
|
+
...current,
|
|
10242
|
+
...next ?? {},
|
|
10243
|
+
fingerprint
|
|
10244
|
+
};
|
|
10245
|
+
}
|
|
10246
|
+
function upsertTaskByFingerprint(input, db) {
|
|
10247
|
+
const d = db || getDatabase();
|
|
10248
|
+
const fingerprint = input.fingerprint.trim();
|
|
10249
|
+
if (!fingerprint)
|
|
10250
|
+
throw new Error("fingerprint is required");
|
|
10251
|
+
const existing = getTaskByFingerprint(fingerprint, d);
|
|
10252
|
+
const metadata = mergeTaskMetadata(existing?.metadata ?? {}, input.metadata, fingerprint);
|
|
10253
|
+
if (!existing) {
|
|
10254
|
+
const task2 = createTask({ ...input, metadata }, d);
|
|
10255
|
+
return { task: task2, created: true };
|
|
10256
|
+
}
|
|
10257
|
+
const task = updateTask(existing.id, {
|
|
10258
|
+
version: existing.version,
|
|
10259
|
+
title: input.title,
|
|
10260
|
+
description: input.description,
|
|
10261
|
+
status: input.status,
|
|
10262
|
+
priority: input.priority,
|
|
10263
|
+
project_id: input.project_id,
|
|
10264
|
+
assigned_to: input.assigned_to,
|
|
10265
|
+
working_dir: input.working_dir,
|
|
10266
|
+
plan_id: input.plan_id,
|
|
10267
|
+
task_list_id: input.task_list_id,
|
|
10268
|
+
tags: input.tags,
|
|
10269
|
+
metadata,
|
|
10270
|
+
due_at: input.due_at,
|
|
10271
|
+
estimated_minutes: input.estimated_minutes,
|
|
10272
|
+
sla_minutes: input.sla_minutes,
|
|
10273
|
+
confidence: input.confidence,
|
|
10274
|
+
retry_count: input.retry_count,
|
|
10275
|
+
max_retries: input.max_retries,
|
|
10276
|
+
retry_after: input.retry_after,
|
|
10277
|
+
requires_approval: input.requires_approval,
|
|
10278
|
+
recurrence_rule: input.recurrence_rule,
|
|
10279
|
+
task_type: input.task_type
|
|
10280
|
+
}, d);
|
|
10281
|
+
return { task, created: false };
|
|
10282
|
+
}
|
|
9950
10283
|
function countTasks(filter = {}, db) {
|
|
9951
10284
|
const d = db || getDatabase();
|
|
9952
10285
|
const conditions = [];
|
|
@@ -10010,6 +10343,7 @@ function countTasks(filter = {}, db) {
|
|
|
10010
10343
|
conditions.push("task_list_id = ?");
|
|
10011
10344
|
params.push(filter.task_list_id);
|
|
10012
10345
|
}
|
|
10346
|
+
addMetadataConditions(filter.metadata, conditions, params);
|
|
10013
10347
|
if (!filter.include_archived) {
|
|
10014
10348
|
conditions.push("archived_at IS NULL");
|
|
10015
10349
|
}
|
|
@@ -10060,6 +10394,10 @@ function updateTask(id, input, db) {
|
|
|
10060
10394
|
sets.push("assigned_to = ?");
|
|
10061
10395
|
params.push(input.assigned_to);
|
|
10062
10396
|
}
|
|
10397
|
+
if (input.working_dir !== undefined) {
|
|
10398
|
+
sets.push("working_dir = ?");
|
|
10399
|
+
params.push(input.working_dir);
|
|
10400
|
+
}
|
|
10063
10401
|
if (input.tags !== undefined) {
|
|
10064
10402
|
sets.push("tags = ?");
|
|
10065
10403
|
params.push(JSON.stringify(input.tags));
|
|
@@ -10148,6 +10486,8 @@ function updateTask(id, input, db) {
|
|
|
10148
10486
|
logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
|
|
10149
10487
|
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
|
|
10150
10488
|
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
10489
|
+
if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
|
|
10490
|
+
logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
|
|
10151
10491
|
if (input.approved_by !== undefined)
|
|
10152
10492
|
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
10153
10493
|
const updatedTask = {
|
|
@@ -10183,6 +10523,10 @@ function updateTask(id, input, db) {
|
|
|
10183
10523
|
if (input.approved_by !== undefined) {
|
|
10184
10524
|
emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
|
|
10185
10525
|
}
|
|
10526
|
+
const updatePayload = taskEventData(updatedTask);
|
|
10527
|
+
dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
|
|
10528
|
+
emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload });
|
|
10529
|
+
emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask });
|
|
10186
10530
|
return updatedTask;
|
|
10187
10531
|
}
|
|
10188
10532
|
function deleteTask(id, db) {
|
|
@@ -13200,6 +13544,72 @@ function rowToArtifact(row) {
|
|
|
13200
13544
|
function getRunRow(runId, db) {
|
|
13201
13545
|
return db.query("SELECT * FROM task_runs WHERE id = ?").get(runId);
|
|
13202
13546
|
}
|
|
13547
|
+
function normalizeTransactionKey(input) {
|
|
13548
|
+
const key = (input.key || input.loop_run_id || input.loop_id || "").trim();
|
|
13549
|
+
if (!key)
|
|
13550
|
+
throw new Error("idempotent run transactions require --key, --loop-run-id, or --loop-id");
|
|
13551
|
+
return key.toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 240);
|
|
13552
|
+
}
|
|
13553
|
+
function loopTransactionMetadata(record) {
|
|
13554
|
+
const value = record.metadata["loop_transaction"];
|
|
13555
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
13556
|
+
}
|
|
13557
|
+
function runKey(record) {
|
|
13558
|
+
const tx = loopTransactionMetadata(record);
|
|
13559
|
+
const key = tx["idempotency_key"] ?? record.metadata["idempotency_key"];
|
|
13560
|
+
return typeof key === "string" ? key : null;
|
|
13561
|
+
}
|
|
13562
|
+
function loopId(record) {
|
|
13563
|
+
const tx = loopTransactionMetadata(record);
|
|
13564
|
+
const value = tx["loop_id"] ?? record.metadata["loop_id"];
|
|
13565
|
+
return typeof value === "string" ? value : null;
|
|
13566
|
+
}
|
|
13567
|
+
function loopRunId(record) {
|
|
13568
|
+
const tx = loopTransactionMetadata(record);
|
|
13569
|
+
const value = tx["loop_run_id"] ?? record.metadata["loop_run_id"];
|
|
13570
|
+
return typeof value === "string" ? value : null;
|
|
13571
|
+
}
|
|
13572
|
+
function getTaskRunTransactionByKey(key, taskId, db) {
|
|
13573
|
+
if (taskId) {
|
|
13574
|
+
return db.query("SELECT * FROM task_run_transactions WHERE task_id = ? AND key = ?").get(taskId, key);
|
|
13575
|
+
}
|
|
13576
|
+
const rows = db.query("SELECT * FROM task_run_transactions WHERE key = ? ORDER BY created_at DESC LIMIT 2").all(key);
|
|
13577
|
+
if (rows.length > 1)
|
|
13578
|
+
throw new Error(`Run transaction key is ambiguous across tasks: ${key}. Pass task_id.`);
|
|
13579
|
+
return rows[0] ?? null;
|
|
13580
|
+
}
|
|
13581
|
+
function summarizeTaskRun(run) {
|
|
13582
|
+
return {
|
|
13583
|
+
id: run.id,
|
|
13584
|
+
task_id: run.task_id,
|
|
13585
|
+
agent_id: run.agent_id,
|
|
13586
|
+
title: run.title,
|
|
13587
|
+
status: run.status,
|
|
13588
|
+
summary: run.summary,
|
|
13589
|
+
idempotency_key: runKey(run),
|
|
13590
|
+
loop_id: loopId(run),
|
|
13591
|
+
loop_run_id: loopRunId(run),
|
|
13592
|
+
metadata_keys: Object.keys(run.metadata).sort(),
|
|
13593
|
+
started_at: run.started_at,
|
|
13594
|
+
completed_at: run.completed_at,
|
|
13595
|
+
updated_at: run.updated_at
|
|
13596
|
+
};
|
|
13597
|
+
}
|
|
13598
|
+
function findTaskRunByTransactionKey(key, taskId, db) {
|
|
13599
|
+
const d = db || getDatabase();
|
|
13600
|
+
const normalized = normalizeTransactionKey({ key });
|
|
13601
|
+
const transaction = getTaskRunTransactionByKey(normalized, taskId, d);
|
|
13602
|
+
if (transaction?.run_id)
|
|
13603
|
+
return getTaskRun(transaction.run_id, d);
|
|
13604
|
+
return null;
|
|
13605
|
+
}
|
|
13606
|
+
function loopRunCommands(run, key) {
|
|
13607
|
+
return [
|
|
13608
|
+
run ? `todos runs show ${run.id.slice(0, 8)}` : "todos runs list",
|
|
13609
|
+
run ? `todos findings list --task ${run.task_id.slice(0, 8)} --json` : "todos findings list --json",
|
|
13610
|
+
`todos runs begin <task-id> --key ${key} --apply --json`
|
|
13611
|
+
];
|
|
13612
|
+
}
|
|
13203
13613
|
function resolveTaskRunId(idOrPrefix, db) {
|
|
13204
13614
|
const d = db || getDatabase();
|
|
13205
13615
|
const rows = d.query("SELECT id FROM task_runs WHERE id = ? OR id LIKE ? ORDER BY created_at DESC LIMIT 2").all(idOrPrefix, `${idOrPrefix}%`);
|
|
@@ -13218,7 +13628,7 @@ function startTaskRun(input, db) {
|
|
|
13218
13628
|
const d = db || getDatabase();
|
|
13219
13629
|
if (!getTask(input.task_id, d))
|
|
13220
13630
|
throw new TaskNotFoundError(input.task_id);
|
|
13221
|
-
const id = uuid();
|
|
13631
|
+
const id = input.id ?? uuid();
|
|
13222
13632
|
const timestamp = input.started_at || now();
|
|
13223
13633
|
if (input.claim && input.agent_id) {
|
|
13224
13634
|
startTask(input.task_id, input.agent_id, d);
|
|
@@ -13256,6 +13666,97 @@ function startTaskRun(input, db) {
|
|
|
13256
13666
|
emitLocalEventHooksQuiet({ type: "run.started", payload: { id: run.id, task_id: run.task_id, agent_id: run.agent_id, title: run.title } });
|
|
13257
13667
|
return run;
|
|
13258
13668
|
}
|
|
13669
|
+
function beginTaskRunTransaction(input, db) {
|
|
13670
|
+
const d = db || getDatabase();
|
|
13671
|
+
if (!getTask(input.task_id, d))
|
|
13672
|
+
throw new TaskNotFoundError(input.task_id);
|
|
13673
|
+
const timestamp = input.started_at || now();
|
|
13674
|
+
const key = normalizeTransactionKey(input);
|
|
13675
|
+
const existing = findTaskRunByTransactionKey(key, input.task_id, d);
|
|
13676
|
+
const dryRun = !input.apply;
|
|
13677
|
+
if (existing) {
|
|
13678
|
+
return {
|
|
13679
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
13680
|
+
local_only: true,
|
|
13681
|
+
dry_run: dryRun,
|
|
13682
|
+
processed_at: timestamp,
|
|
13683
|
+
action: "matched",
|
|
13684
|
+
key,
|
|
13685
|
+
run: summarizeTaskRun(existing),
|
|
13686
|
+
warnings: existing.status === "running" ? [] : [`matched ${existing.status} run`],
|
|
13687
|
+
commands: loopRunCommands(existing, key)
|
|
13688
|
+
};
|
|
13689
|
+
}
|
|
13690
|
+
if (dryRun) {
|
|
13691
|
+
return {
|
|
13692
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
13693
|
+
local_only: true,
|
|
13694
|
+
dry_run: true,
|
|
13695
|
+
processed_at: timestamp,
|
|
13696
|
+
action: "preview",
|
|
13697
|
+
key,
|
|
13698
|
+
run: null,
|
|
13699
|
+
warnings: [],
|
|
13700
|
+
commands: loopRunCommands(null, key)
|
|
13701
|
+
};
|
|
13702
|
+
}
|
|
13703
|
+
const metadata = redactValue({
|
|
13704
|
+
...input.metadata || {},
|
|
13705
|
+
loop_transaction: {
|
|
13706
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
13707
|
+
idempotency_key: key,
|
|
13708
|
+
loop_id: input.loop_id ?? null,
|
|
13709
|
+
loop_run_id: input.loop_run_id ?? null,
|
|
13710
|
+
first_seen_at: timestamp
|
|
13711
|
+
},
|
|
13712
|
+
idempotency_key: key
|
|
13713
|
+
});
|
|
13714
|
+
const created = d.transaction(() => {
|
|
13715
|
+
d.run(`INSERT OR IGNORE INTO task_run_transactions (
|
|
13716
|
+
id, task_id, run_id, key, loop_id, loop_run_id, metadata, created_at, updated_at
|
|
13717
|
+
) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?)`, [
|
|
13718
|
+
uuid(),
|
|
13719
|
+
input.task_id,
|
|
13720
|
+
key,
|
|
13721
|
+
input.loop_id ?? null,
|
|
13722
|
+
input.loop_run_id ?? null,
|
|
13723
|
+
JSON.stringify(metadata),
|
|
13724
|
+
timestamp,
|
|
13725
|
+
timestamp
|
|
13726
|
+
]);
|
|
13727
|
+
const transaction = getTaskRunTransactionByKey(key, input.task_id, d);
|
|
13728
|
+
if (!transaction)
|
|
13729
|
+
throw new Error(`Could not create run transaction for key: ${key}`);
|
|
13730
|
+
if (transaction.run_id) {
|
|
13731
|
+
const existingRun = getTaskRun(transaction.run_id, d);
|
|
13732
|
+
if (existingRun)
|
|
13733
|
+
return { run: existingRun, action: "matched" };
|
|
13734
|
+
}
|
|
13735
|
+
const run = startTaskRun({
|
|
13736
|
+
id: uuid(),
|
|
13737
|
+
task_id: input.task_id,
|
|
13738
|
+
agent_id: input.agent_id,
|
|
13739
|
+
title: input.title,
|
|
13740
|
+
summary: input.summary,
|
|
13741
|
+
metadata,
|
|
13742
|
+
claim: input.claim,
|
|
13743
|
+
started_at: timestamp
|
|
13744
|
+
}, d);
|
|
13745
|
+
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]);
|
|
13746
|
+
return { run, action: "created" };
|
|
13747
|
+
})();
|
|
13748
|
+
return {
|
|
13749
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
13750
|
+
local_only: true,
|
|
13751
|
+
dry_run: false,
|
|
13752
|
+
processed_at: timestamp,
|
|
13753
|
+
action: created.action,
|
|
13754
|
+
key,
|
|
13755
|
+
run: summarizeTaskRun(created.run),
|
|
13756
|
+
warnings: [],
|
|
13757
|
+
commands: loopRunCommands(created.run, key)
|
|
13758
|
+
};
|
|
13759
|
+
}
|
|
13259
13760
|
function addTaskRunEvent(input, db) {
|
|
13260
13761
|
const d = db || getDatabase();
|
|
13261
13762
|
const runId = resolveTaskRunId(input.run_id, d);
|
|
@@ -13435,6 +13936,66 @@ function finishTaskRun(input, db) {
|
|
|
13435
13936
|
});
|
|
13436
13937
|
return updated;
|
|
13437
13938
|
}
|
|
13939
|
+
function finishTaskRunTransaction(input, db) {
|
|
13940
|
+
const d = db || getDatabase();
|
|
13941
|
+
const timestamp = input.completed_at || now();
|
|
13942
|
+
const status = input.status || "completed";
|
|
13943
|
+
const key = input.key ? normalizeTransactionKey({ key: input.key }) : "";
|
|
13944
|
+
const run = input.run_id ? getTaskRun(resolveTaskRunId(input.run_id, d), d) : key ? findTaskRunByTransactionKey(key, input.task_id, d) : null;
|
|
13945
|
+
if (!run) {
|
|
13946
|
+
throw new Error(input.run_id ? `Run not found: ${input.run_id}` : "runs finish requires a run id or --key");
|
|
13947
|
+
}
|
|
13948
|
+
if (input.task_id && run.task_id !== input.task_id) {
|
|
13949
|
+
throw new Error(`Run ${run.id} belongs to task ${run.task_id}, not ${input.task_id}`);
|
|
13950
|
+
}
|
|
13951
|
+
const resolvedKey = key || runKey(run) || run.id;
|
|
13952
|
+
const dryRun = input.apply === false;
|
|
13953
|
+
if (run.status !== "running") {
|
|
13954
|
+
const conflict = run.status !== status;
|
|
13955
|
+
return {
|
|
13956
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
13957
|
+
local_only: true,
|
|
13958
|
+
dry_run: dryRun,
|
|
13959
|
+
processed_at: timestamp,
|
|
13960
|
+
action: conflict ? "conflict" : "matched",
|
|
13961
|
+
key: resolvedKey,
|
|
13962
|
+
run: summarizeTaskRun(run),
|
|
13963
|
+
warnings: conflict ? [`run is already ${run.status}; requested ${status}`] : [],
|
|
13964
|
+
commands: loopRunCommands(run, resolvedKey)
|
|
13965
|
+
};
|
|
13966
|
+
}
|
|
13967
|
+
if (dryRun) {
|
|
13968
|
+
return {
|
|
13969
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
13970
|
+
local_only: true,
|
|
13971
|
+
dry_run: true,
|
|
13972
|
+
processed_at: timestamp,
|
|
13973
|
+
action: "preview",
|
|
13974
|
+
key: resolvedKey,
|
|
13975
|
+
run: summarizeTaskRun({ ...run, status, summary: input.summary ?? run.summary, completed_at: timestamp, updated_at: timestamp }),
|
|
13976
|
+
warnings: [],
|
|
13977
|
+
commands: loopRunCommands(run, resolvedKey)
|
|
13978
|
+
};
|
|
13979
|
+
}
|
|
13980
|
+
const finished = finishTaskRun({
|
|
13981
|
+
run_id: run.id,
|
|
13982
|
+
status,
|
|
13983
|
+
summary: input.summary,
|
|
13984
|
+
agent_id: input.agent_id,
|
|
13985
|
+
completed_at: timestamp
|
|
13986
|
+
}, d);
|
|
13987
|
+
return {
|
|
13988
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
13989
|
+
local_only: true,
|
|
13990
|
+
dry_run: false,
|
|
13991
|
+
processed_at: timestamp,
|
|
13992
|
+
action: "finished",
|
|
13993
|
+
key: resolvedKey,
|
|
13994
|
+
run: summarizeTaskRun(finished),
|
|
13995
|
+
warnings: [],
|
|
13996
|
+
commands: loopRunCommands(finished, resolvedKey)
|
|
13997
|
+
};
|
|
13998
|
+
}
|
|
13438
13999
|
function listTaskRuns(taskId, db) {
|
|
13439
14000
|
const d = db || getDatabase();
|
|
13440
14001
|
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();
|
|
@@ -13452,6 +14013,7 @@ function getTaskRunLedger(runId, db) {
|
|
|
13452
14013
|
const files = d.query("SELECT * FROM task_files WHERE task_id = ? ORDER BY updated_at DESC, path").all(run.task_id);
|
|
13453
14014
|
return { run, events, commands, artifacts, files };
|
|
13454
14015
|
}
|
|
14016
|
+
var LOOP_RUN_TRANSACTION_SCHEMA_VERSION = "todos.loop_run_transaction.v1";
|
|
13455
14017
|
var init_task_runs = __esm(() => {
|
|
13456
14018
|
init_artifact_store();
|
|
13457
14019
|
init_event_hooks();
|
|
@@ -13840,6 +14402,7 @@ var init_calendar = __esm(() => {
|
|
|
13840
14402
|
var exports_tasks = {};
|
|
13841
14403
|
__export(exports_tasks, {
|
|
13842
14404
|
watchTask: () => watchTask,
|
|
14405
|
+
upsertTaskByFingerprint: () => upsertTaskByFingerprint,
|
|
13843
14406
|
updateTaskBoard: () => updateTaskBoard,
|
|
13844
14407
|
updateTask: () => updateTask,
|
|
13845
14408
|
unwatchTask: () => unwatchTask,
|
|
@@ -13883,6 +14446,7 @@ __export(exports_tasks, {
|
|
|
13883
14446
|
getTaskGraph: () => getTaskGraph,
|
|
13884
14447
|
getTaskDependents: () => getTaskDependents,
|
|
13885
14448
|
getTaskDependencies: () => getTaskDependencies,
|
|
14449
|
+
getTaskByFingerprint: () => getTaskByFingerprint,
|
|
13886
14450
|
getTaskBoard: () => getTaskBoard,
|
|
13887
14451
|
getTask: () => getTask,
|
|
13888
14452
|
getStatus: () => getStatus,
|
|
@@ -14171,90 +14735,6 @@ var init_dispatch = __esm(() => {
|
|
|
14171
14735
|
init_tmux();
|
|
14172
14736
|
});
|
|
14173
14737
|
|
|
14174
|
-
// src/db/task-lists.ts
|
|
14175
|
-
function rowToTaskList(row) {
|
|
14176
|
-
return {
|
|
14177
|
-
...row,
|
|
14178
|
-
metadata: JSON.parse(row.metadata || "{}")
|
|
14179
|
-
};
|
|
14180
|
-
}
|
|
14181
|
-
function createTaskList(input, db) {
|
|
14182
|
-
const d = db || getDatabase();
|
|
14183
|
-
const id = uuid();
|
|
14184
|
-
const timestamp = now();
|
|
14185
|
-
const slug = input.slug || slugify(input.name);
|
|
14186
|
-
if (!input.project_id) {
|
|
14187
|
-
const existing = d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ?").get(slug);
|
|
14188
|
-
if (existing) {
|
|
14189
|
-
throw new Error(`Standalone task list with slug "${slug}" already exists`);
|
|
14190
|
-
}
|
|
14191
|
-
}
|
|
14192
|
-
d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at)
|
|
14193
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp]);
|
|
14194
|
-
return getTaskList(id, d);
|
|
14195
|
-
}
|
|
14196
|
-
function getTaskList(id, db) {
|
|
14197
|
-
const d = db || getDatabase();
|
|
14198
|
-
const row = d.query("SELECT * FROM task_lists WHERE id = ?").get(id);
|
|
14199
|
-
return row ? rowToTaskList(row) : null;
|
|
14200
|
-
}
|
|
14201
|
-
function getTaskListBySlug(slug, projectId, db) {
|
|
14202
|
-
const d = db || getDatabase();
|
|
14203
|
-
let row;
|
|
14204
|
-
if (projectId) {
|
|
14205
|
-
row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id = ?").get(slug, projectId);
|
|
14206
|
-
} else {
|
|
14207
|
-
row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id IS NULL").get(slug);
|
|
14208
|
-
}
|
|
14209
|
-
return row ? rowToTaskList(row) : null;
|
|
14210
|
-
}
|
|
14211
|
-
function listTaskLists(projectId, db) {
|
|
14212
|
-
const d = db || getDatabase();
|
|
14213
|
-
if (projectId) {
|
|
14214
|
-
return d.query("SELECT * FROM task_lists WHERE project_id = ? ORDER BY name").all(projectId).map(rowToTaskList);
|
|
14215
|
-
}
|
|
14216
|
-
return d.query("SELECT * FROM task_lists ORDER BY name").all().map(rowToTaskList);
|
|
14217
|
-
}
|
|
14218
|
-
function updateTaskList(id, input, db) {
|
|
14219
|
-
const d = db || getDatabase();
|
|
14220
|
-
const existing = getTaskList(id, d);
|
|
14221
|
-
if (!existing)
|
|
14222
|
-
throw new TaskListNotFoundError(id);
|
|
14223
|
-
const sets = ["updated_at = ?"];
|
|
14224
|
-
const params = [now()];
|
|
14225
|
-
if (input.name !== undefined) {
|
|
14226
|
-
sets.push("name = ?");
|
|
14227
|
-
params.push(input.name);
|
|
14228
|
-
}
|
|
14229
|
-
if (input.description !== undefined) {
|
|
14230
|
-
sets.push("description = ?");
|
|
14231
|
-
params.push(input.description);
|
|
14232
|
-
}
|
|
14233
|
-
if (input.metadata !== undefined) {
|
|
14234
|
-
sets.push("metadata = ?");
|
|
14235
|
-
params.push(JSON.stringify(input.metadata));
|
|
14236
|
-
}
|
|
14237
|
-
params.push(id);
|
|
14238
|
-
d.run(`UPDATE task_lists SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
14239
|
-
return getTaskList(id, d);
|
|
14240
|
-
}
|
|
14241
|
-
function deleteTaskList(id, db) {
|
|
14242
|
-
const d = db || getDatabase();
|
|
14243
|
-
return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
|
|
14244
|
-
}
|
|
14245
|
-
function ensureTaskList(name, slug, projectId, db) {
|
|
14246
|
-
const d = db || getDatabase();
|
|
14247
|
-
const existing = getTaskListBySlug(slug, projectId, d);
|
|
14248
|
-
if (existing)
|
|
14249
|
-
return existing;
|
|
14250
|
-
return createTaskList({ name, slug, project_id: projectId }, d);
|
|
14251
|
-
}
|
|
14252
|
-
var init_task_lists = __esm(() => {
|
|
14253
|
-
init_types();
|
|
14254
|
-
init_database();
|
|
14255
|
-
init_projects();
|
|
14256
|
-
});
|
|
14257
|
-
|
|
14258
14738
|
// src/mcp/tools/dispatch.ts
|
|
14259
14739
|
function registerDispatchTools(server, { shouldRegisterTool, resolveId, formatError }) {
|
|
14260
14740
|
if (shouldRegisterTool("dispatch_tasks")) {
|
|
@@ -14813,6 +15293,7 @@ var init_token_utils = __esm(() => {
|
|
|
14813
15293
|
"add_task_run_event",
|
|
14814
15294
|
"add_task_run_file",
|
|
14815
15295
|
"acknowledge_handoff",
|
|
15296
|
+
"begin_task_run_transaction",
|
|
14816
15297
|
"build_local_report",
|
|
14817
15298
|
"cancel_agent_run_dispatch",
|
|
14818
15299
|
"finish_task_run",
|
|
@@ -14860,6 +15341,7 @@ var init_token_utils = __esm(() => {
|
|
|
14860
15341
|
"list_local_snapshots",
|
|
14861
15342
|
"list_retrospectives",
|
|
14862
15343
|
"list_risks",
|
|
15344
|
+
"list_task_findings",
|
|
14863
15345
|
"list_task_runs",
|
|
14864
15346
|
"list_verification_providers",
|
|
14865
15347
|
"merge_duplicate_task",
|
|
@@ -14868,6 +15350,7 @@ var init_token_utils = __esm(() => {
|
|
|
14868
15350
|
"remove_review_routing_rule",
|
|
14869
15351
|
"restore_local_backup",
|
|
14870
15352
|
"retry_agent_run_dispatch",
|
|
15353
|
+
"resolve_missing_task_findings",
|
|
14871
15354
|
"resolve_mentions",
|
|
14872
15355
|
"run_next_agent_dispatch",
|
|
14873
15356
|
"search_knowledge_records",
|
|
@@ -14910,9 +15393,17 @@ var init_token_utils = __esm(() => {
|
|
|
14910
15393
|
"unlock_file",
|
|
14911
15394
|
"unwatch_task",
|
|
14912
15395
|
"update_comment",
|
|
15396
|
+
"upsert_task_finding",
|
|
14913
15397
|
"update_risk",
|
|
14914
15398
|
"watch_task"
|
|
14915
15399
|
],
|
|
15400
|
+
loops: [
|
|
15401
|
+
"begin_task_run_transaction",
|
|
15402
|
+
"finish_task_run",
|
|
15403
|
+
"list_task_findings",
|
|
15404
|
+
"resolve_missing_task_findings",
|
|
15405
|
+
"upsert_task_finding"
|
|
15406
|
+
],
|
|
14916
15407
|
agents: [
|
|
14917
15408
|
"auto_assign_task",
|
|
14918
15409
|
"delete_agent",
|
|
@@ -14994,7 +15485,7 @@ var init_token_utils = __esm(() => {
|
|
|
14994
15485
|
maintenance: ["extract_todos", "get_sla_breaches", "notify_upcoming_deadlines", "run_doctor", "score_task", "watch_source_todos"]
|
|
14995
15486
|
};
|
|
14996
15487
|
MCP_PROFILE_GROUPS = {
|
|
14997
|
-
minimal: ["core"],
|
|
15488
|
+
minimal: ["core", "loops"],
|
|
14998
15489
|
core: ["core"],
|
|
14999
15490
|
standard: ["core", "tasks", "projects", "resources", "agents", "metadata"],
|
|
15000
15491
|
agent: ["core", "tasks", "projects", "resources"],
|
|
@@ -15074,6 +15565,61 @@ function registerTaskCrudTools(server, ctx) {
|
|
|
15074
15565
|
}
|
|
15075
15566
|
});
|
|
15076
15567
|
}
|
|
15568
|
+
if (shouldRegisterTool("upsert_task")) {
|
|
15569
|
+
server.tool("upsert_task", "Create or update a task by stable metadata fingerprint. Metadata is shallow-merged on updates.", {
|
|
15570
|
+
fingerprint: exports_external.string().describe("Stable dedupe fingerprint stored as metadata.fingerprint"),
|
|
15571
|
+
title: exports_external.string().describe("Task title"),
|
|
15572
|
+
description: exports_external.string().optional().describe("Task description"),
|
|
15573
|
+
status: exports_external.enum(["pending", "in_progress", "completed", "failed", "cancelled"]).optional(),
|
|
15574
|
+
priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
|
|
15575
|
+
project_id: exports_external.string().optional().describe("Project ID"),
|
|
15576
|
+
task_list_id: exports_external.string().optional().describe("Task list ID"),
|
|
15577
|
+
assigned_to: exports_external.string().optional().describe("Agent ID or name to assign to"),
|
|
15578
|
+
tags: exports_external.array(exports_external.string()).optional().describe("Tags for the task"),
|
|
15579
|
+
working_dir: exports_external.string().optional().describe("Working directory associated with the task"),
|
|
15580
|
+
metadata: exports_external.record(exports_external.unknown()).optional().describe("Metadata object to shallow-merge"),
|
|
15581
|
+
expectation_id: exports_external.string().optional(),
|
|
15582
|
+
expectation_fingerprint: exports_external.string().optional(),
|
|
15583
|
+
evidence_paths: exports_external.array(exports_external.string()).optional(),
|
|
15584
|
+
origin_loop_id: exports_external.string().optional(),
|
|
15585
|
+
origin_run_id: exports_external.string().optional(),
|
|
15586
|
+
expected: exports_external.unknown().optional(),
|
|
15587
|
+
observed: exports_external.unknown().optional(),
|
|
15588
|
+
acceptance: exports_external.unknown().optional()
|
|
15589
|
+
}, async (params) => {
|
|
15590
|
+
try {
|
|
15591
|
+
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;
|
|
15592
|
+
const mergedMetadata = { ...metadata ?? {} };
|
|
15593
|
+
if (expectation_id !== undefined)
|
|
15594
|
+
mergedMetadata["expectation_id"] = expectation_id;
|
|
15595
|
+
if (expectation_fingerprint !== undefined)
|
|
15596
|
+
mergedMetadata["expectation_fingerprint"] = expectation_fingerprint;
|
|
15597
|
+
if (evidence_paths !== undefined)
|
|
15598
|
+
mergedMetadata["evidence_paths"] = evidence_paths;
|
|
15599
|
+
if (origin_loop_id !== undefined)
|
|
15600
|
+
mergedMetadata["origin_loop_id"] = origin_loop_id;
|
|
15601
|
+
if (origin_run_id !== undefined)
|
|
15602
|
+
mergedMetadata["origin_run_id"] = origin_run_id;
|
|
15603
|
+
if (expected !== undefined)
|
|
15604
|
+
mergedMetadata["expected"] = expected;
|
|
15605
|
+
if (observed !== undefined)
|
|
15606
|
+
mergedMetadata["observed"] = observed;
|
|
15607
|
+
if (acceptance !== undefined)
|
|
15608
|
+
mergedMetadata["acceptance"] = acceptance;
|
|
15609
|
+
const resolved = { ...rest, metadata: mergedMetadata };
|
|
15610
|
+
if (assigned_to)
|
|
15611
|
+
resolved.assigned_to = resolveAssignee(assigned_to);
|
|
15612
|
+
if (project_id)
|
|
15613
|
+
resolved.project_id = resolveId(project_id, "projects");
|
|
15614
|
+
if (task_list_id)
|
|
15615
|
+
resolved.task_list_id = resolveId(task_list_id, "task_lists");
|
|
15616
|
+
const result = upsertTaskByFingerprint(resolved);
|
|
15617
|
+
return { content: [{ type: "text", text: compactJson({ created: result.created, task: JSON.parse(mutationTaskResponse(result.task)) }) }] };
|
|
15618
|
+
} catch (e) {
|
|
15619
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
15620
|
+
}
|
|
15621
|
+
});
|
|
15622
|
+
}
|
|
15077
15623
|
if (shouldRegisterTool("list_tasks")) {
|
|
15078
15624
|
server.tool("list_tasks", "List tasks with optional filters. Pass empty arrays for multi-value filters (e.g. status=[] shows all).", {
|
|
15079
15625
|
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"),
|
|
@@ -15085,7 +15631,8 @@ function registerTaskCrudTools(server, ctx) {
|
|
|
15085
15631
|
created_after: exports_external.string().optional().describe("ISO date \u2014 tasks created after this date"),
|
|
15086
15632
|
created_before: exports_external.string().optional().describe("ISO date \u2014 tasks created before this date"),
|
|
15087
15633
|
limit: exports_external.number().optional().describe("Max results (default: 50, max 500)"),
|
|
15088
|
-
offset: exports_external.number().optional().describe("Pagination offset")
|
|
15634
|
+
offset: exports_external.number().optional().describe("Pagination offset"),
|
|
15635
|
+
metadata: exports_external.record(exports_external.unknown()).optional().describe("Exact top-level metadata filters")
|
|
15089
15636
|
}, async (params) => {
|
|
15090
15637
|
try {
|
|
15091
15638
|
const resolved = { ...params };
|
|
@@ -18626,7 +19173,7 @@ function countMovedStandaloneVerifications(db, duplicateId) {
|
|
|
18626
19173
|
const rows = db.query("SELECT command FROM task_verifications WHERE task_id = ?").all(duplicateId);
|
|
18627
19174
|
return rows.filter((row) => !generatedCommands.has(row.command)).length;
|
|
18628
19175
|
}
|
|
18629
|
-
function
|
|
19176
|
+
function mergeTaskMetadata2(primary, duplicate, input, mergedAt) {
|
|
18630
19177
|
const mergedDuplicates = Array.isArray(primary.metadata["merged_duplicates"]) ? [...primary.metadata["merged_duplicates"]] : [];
|
|
18631
19178
|
mergedDuplicates.push({
|
|
18632
19179
|
id: duplicate.id,
|
|
@@ -18687,7 +19234,7 @@ function mergeDuplicateTask(input, db) {
|
|
|
18687
19234
|
updateTask(primary.id, {
|
|
18688
19235
|
version: primary.version,
|
|
18689
19236
|
tags: mergedTags,
|
|
18690
|
-
metadata:
|
|
19237
|
+
metadata: mergeTaskMetadata2(primary, duplicate, input, mergedAt),
|
|
18691
19238
|
description: mergeTaskDescription(primary, duplicate) ?? undefined
|
|
18692
19239
|
}, d);
|
|
18693
19240
|
moved.comments = updateRows(d, "task_comments", "task_id", duplicate.id, primary.id);
|
|
@@ -25484,6 +26031,356 @@ var init_task_meta_tools = __esm(() => {
|
|
|
25484
26031
|
init_zod();
|
|
25485
26032
|
});
|
|
25486
26033
|
|
|
26034
|
+
// src/db/findings.ts
|
|
26035
|
+
function parseObject2(value) {
|
|
26036
|
+
if (!value)
|
|
26037
|
+
return {};
|
|
26038
|
+
try {
|
|
26039
|
+
const parsed = JSON.parse(value);
|
|
26040
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
26041
|
+
} catch {
|
|
26042
|
+
return {};
|
|
26043
|
+
}
|
|
26044
|
+
}
|
|
26045
|
+
function normalizeKey(value) {
|
|
26046
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
26047
|
+
}
|
|
26048
|
+
function normalizeFingerprint(value) {
|
|
26049
|
+
const normalized = normalizeKey(value);
|
|
26050
|
+
if (!normalized)
|
|
26051
|
+
throw new Error("finding fingerprint is required");
|
|
26052
|
+
return normalized.slice(0, 240);
|
|
26053
|
+
}
|
|
26054
|
+
function normalizeSeverity(value) {
|
|
26055
|
+
const normalized = normalizeKey(value || "medium");
|
|
26056
|
+
if (SEVERITIES.has(normalized))
|
|
26057
|
+
return normalized;
|
|
26058
|
+
if (/^(p0|blocker|urgent|highest)$/.test(normalized))
|
|
26059
|
+
return "critical";
|
|
26060
|
+
if (/^(p1|major)$/.test(normalized))
|
|
26061
|
+
return "high";
|
|
26062
|
+
if (/^(p3|minor|info)$/.test(normalized))
|
|
26063
|
+
return "low";
|
|
26064
|
+
return "medium";
|
|
26065
|
+
}
|
|
26066
|
+
function normalizeStatus(value) {
|
|
26067
|
+
const normalized = normalizeKey(value || "open");
|
|
26068
|
+
if (STATUSES.has(normalized))
|
|
26069
|
+
return normalized;
|
|
26070
|
+
if (normalized === "closed" || normalized === "fixed")
|
|
26071
|
+
return "resolved";
|
|
26072
|
+
return "open";
|
|
26073
|
+
}
|
|
26074
|
+
function normalizeResolutionStatus(value) {
|
|
26075
|
+
const status = normalizeStatus(value || "resolved");
|
|
26076
|
+
if (status === "open")
|
|
26077
|
+
throw new Error("resolve-missing status must be resolved or ignored");
|
|
26078
|
+
return status;
|
|
26079
|
+
}
|
|
26080
|
+
function redactOptional(value, max = 2000) {
|
|
26081
|
+
if (!value)
|
|
26082
|
+
return null;
|
|
26083
|
+
const redacted = redactEvidenceText(value).trim();
|
|
26084
|
+
if (!redacted)
|
|
26085
|
+
return null;
|
|
26086
|
+
return redacted.length > max ? `${redacted.slice(0, max - 3)}...` : redacted;
|
|
26087
|
+
}
|
|
26088
|
+
function rowToFinding(row) {
|
|
26089
|
+
return {
|
|
26090
|
+
schema_version: TASK_FINDING_SCHEMA_VERSION,
|
|
26091
|
+
...row,
|
|
26092
|
+
severity: normalizeSeverity(row.severity),
|
|
26093
|
+
status: normalizeStatus(row.status),
|
|
26094
|
+
metadata: parseObject2(row.metadata)
|
|
26095
|
+
};
|
|
26096
|
+
}
|
|
26097
|
+
function compactFinding(finding2) {
|
|
26098
|
+
return {
|
|
26099
|
+
schema_version: TASK_FINDING_SCHEMA_VERSION,
|
|
26100
|
+
id: finding2.id,
|
|
26101
|
+
task_id: finding2.task_id,
|
|
26102
|
+
run_id: finding2.run_id,
|
|
26103
|
+
fingerprint: finding2.fingerprint,
|
|
26104
|
+
title: finding2.title,
|
|
26105
|
+
severity: finding2.severity,
|
|
26106
|
+
status: finding2.status,
|
|
26107
|
+
source: finding2.source,
|
|
26108
|
+
summary: finding2.summary,
|
|
26109
|
+
artifact_path: finding2.artifact_path,
|
|
26110
|
+
first_seen_at: finding2.first_seen_at,
|
|
26111
|
+
last_seen_at: finding2.last_seen_at,
|
|
26112
|
+
resolved_at: finding2.resolved_at,
|
|
26113
|
+
metadata_keys: Object.keys(finding2.metadata).sort()
|
|
26114
|
+
};
|
|
26115
|
+
}
|
|
26116
|
+
function previewFinding(existing, next, timestamp3) {
|
|
26117
|
+
return {
|
|
26118
|
+
...existing,
|
|
26119
|
+
run_id: next.run_id,
|
|
26120
|
+
title: next.title,
|
|
26121
|
+
severity: next.severity,
|
|
26122
|
+
status: next.status,
|
|
26123
|
+
source: next.source,
|
|
26124
|
+
summary: next.summary,
|
|
26125
|
+
artifact_path: next.artifact_path,
|
|
26126
|
+
metadata: next.metadata,
|
|
26127
|
+
last_seen_at: timestamp3,
|
|
26128
|
+
resolved_at: next.status === "open" ? null : existing.resolved_at || timestamp3,
|
|
26129
|
+
updated_at: timestamp3
|
|
26130
|
+
};
|
|
26131
|
+
}
|
|
26132
|
+
function upsertAction(existing, next) {
|
|
26133
|
+
if (sameFinding(existing, next))
|
|
26134
|
+
return "matched";
|
|
26135
|
+
return existing.status !== "open" && next.status === "open" ? "reopened" : "updated";
|
|
26136
|
+
}
|
|
26137
|
+
function resolveRunForTask(runId, taskId, db) {
|
|
26138
|
+
if (!runId)
|
|
26139
|
+
return null;
|
|
26140
|
+
const resolved = resolveTaskRunId(runId, db);
|
|
26141
|
+
const run = getTaskRun(resolved, db);
|
|
26142
|
+
if (!run)
|
|
26143
|
+
throw new Error(`Run not found: ${runId}`);
|
|
26144
|
+
if (run.task_id !== taskId)
|
|
26145
|
+
throw new Error(`Run ${resolved} belongs to task ${run.task_id}, not ${taskId}`);
|
|
26146
|
+
return resolved;
|
|
26147
|
+
}
|
|
26148
|
+
function getFindingByFingerprint(taskId, fingerprint2, db) {
|
|
26149
|
+
const row = db.query("SELECT * FROM task_findings WHERE task_id = ? AND fingerprint = ?").get(taskId, fingerprint2);
|
|
26150
|
+
return row ? rowToFinding(row) : null;
|
|
26151
|
+
}
|
|
26152
|
+
function assertTask(taskId, db) {
|
|
26153
|
+
if (!getTask(taskId, db))
|
|
26154
|
+
throw new TaskNotFoundError(taskId);
|
|
26155
|
+
}
|
|
26156
|
+
function nextFinding(input, db) {
|
|
26157
|
+
const fingerprint2 = normalizeFingerprint(input.fingerprint);
|
|
26158
|
+
const title = redactOptional(input.title, 300);
|
|
26159
|
+
if (!title)
|
|
26160
|
+
throw new Error("finding title is required");
|
|
26161
|
+
return {
|
|
26162
|
+
fingerprint: fingerprint2,
|
|
26163
|
+
run_id: resolveRunForTask(input.run_id, input.task_id, db),
|
|
26164
|
+
title,
|
|
26165
|
+
severity: normalizeSeverity(input.severity),
|
|
26166
|
+
status: normalizeStatus(input.status),
|
|
26167
|
+
source: redactOptional(input.source, 120),
|
|
26168
|
+
summary: redactOptional(input.summary, 2000),
|
|
26169
|
+
artifact_path: redactOptional(input.artifact_path, 1000),
|
|
26170
|
+
metadata: redactValue(input.metadata || {})
|
|
26171
|
+
};
|
|
26172
|
+
}
|
|
26173
|
+
function sameFinding(left, right) {
|
|
26174
|
+
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);
|
|
26175
|
+
}
|
|
26176
|
+
function upsertTaskFinding(input, db) {
|
|
26177
|
+
const d = db || getDatabase();
|
|
26178
|
+
assertTask(input.task_id, d);
|
|
26179
|
+
const timestamp3 = input.observed_at || now();
|
|
26180
|
+
const warnings = [];
|
|
26181
|
+
const next = nextFinding(input, d);
|
|
26182
|
+
const existing = getFindingByFingerprint(input.task_id, next.fingerprint, d);
|
|
26183
|
+
const dryRun = !input.apply;
|
|
26184
|
+
if (dryRun) {
|
|
26185
|
+
const action2 = existing ? upsertAction(existing, next) : "preview";
|
|
26186
|
+
return {
|
|
26187
|
+
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
26188
|
+
local_only: true,
|
|
26189
|
+
dry_run: true,
|
|
26190
|
+
processed_at: timestamp3,
|
|
26191
|
+
action: action2,
|
|
26192
|
+
fingerprint: next.fingerprint,
|
|
26193
|
+
finding: existing ? compactFinding(previewFinding(existing, next, timestamp3)) : null,
|
|
26194
|
+
warnings
|
|
26195
|
+
};
|
|
26196
|
+
}
|
|
26197
|
+
if (!existing) {
|
|
26198
|
+
const id = uuid();
|
|
26199
|
+
d.run(`INSERT INTO task_findings (
|
|
26200
|
+
id, task_id, run_id, fingerprint, title, severity, status, source, summary, artifact_path,
|
|
26201
|
+
metadata, first_seen_at, last_seen_at, resolved_at, created_at, updated_at
|
|
26202
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
26203
|
+
id,
|
|
26204
|
+
input.task_id,
|
|
26205
|
+
next.run_id,
|
|
26206
|
+
next.fingerprint,
|
|
26207
|
+
next.title,
|
|
26208
|
+
next.severity,
|
|
26209
|
+
next.status,
|
|
26210
|
+
next.source,
|
|
26211
|
+
next.summary,
|
|
26212
|
+
next.artifact_path,
|
|
26213
|
+
JSON.stringify(next.metadata),
|
|
26214
|
+
timestamp3,
|
|
26215
|
+
timestamp3,
|
|
26216
|
+
next.status === "open" ? null : timestamp3,
|
|
26217
|
+
timestamp3,
|
|
26218
|
+
timestamp3
|
|
26219
|
+
]);
|
|
26220
|
+
return {
|
|
26221
|
+
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
26222
|
+
local_only: true,
|
|
26223
|
+
dry_run: false,
|
|
26224
|
+
processed_at: timestamp3,
|
|
26225
|
+
action: "created",
|
|
26226
|
+
fingerprint: next.fingerprint,
|
|
26227
|
+
finding: compactFinding(getFindingByFingerprint(input.task_id, next.fingerprint, d)),
|
|
26228
|
+
warnings
|
|
26229
|
+
};
|
|
26230
|
+
}
|
|
26231
|
+
if (sameFinding(existing, next)) {
|
|
26232
|
+
return {
|
|
26233
|
+
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
26234
|
+
local_only: true,
|
|
26235
|
+
dry_run: false,
|
|
26236
|
+
processed_at: timestamp3,
|
|
26237
|
+
action: "matched",
|
|
26238
|
+
fingerprint: next.fingerprint,
|
|
26239
|
+
finding: compactFinding(existing),
|
|
26240
|
+
warnings
|
|
26241
|
+
};
|
|
26242
|
+
}
|
|
26243
|
+
const action = upsertAction(existing, next);
|
|
26244
|
+
d.run(`UPDATE task_findings SET
|
|
26245
|
+
run_id = ?, title = ?, severity = ?, status = ?, source = ?, summary = ?, artifact_path = ?,
|
|
26246
|
+
metadata = ?, last_seen_at = ?, resolved_at = ?, updated_at = ?
|
|
26247
|
+
WHERE id = ?`, [
|
|
26248
|
+
next.run_id,
|
|
26249
|
+
next.title,
|
|
26250
|
+
next.severity,
|
|
26251
|
+
next.status,
|
|
26252
|
+
next.source,
|
|
26253
|
+
next.summary,
|
|
26254
|
+
next.artifact_path,
|
|
26255
|
+
JSON.stringify(next.metadata),
|
|
26256
|
+
timestamp3,
|
|
26257
|
+
next.status === "open" ? null : existing.resolved_at || timestamp3,
|
|
26258
|
+
timestamp3,
|
|
26259
|
+
existing.id
|
|
26260
|
+
]);
|
|
26261
|
+
return {
|
|
26262
|
+
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
26263
|
+
local_only: true,
|
|
26264
|
+
dry_run: false,
|
|
26265
|
+
processed_at: timestamp3,
|
|
26266
|
+
action,
|
|
26267
|
+
fingerprint: next.fingerprint,
|
|
26268
|
+
finding: compactFinding(getFindingByFingerprint(input.task_id, next.fingerprint, d)),
|
|
26269
|
+
warnings
|
|
26270
|
+
};
|
|
26271
|
+
}
|
|
26272
|
+
function listTaskFindings(filter = {}, db) {
|
|
26273
|
+
const d = db || getDatabase();
|
|
26274
|
+
const conditions = ["1=1"];
|
|
26275
|
+
const params = [];
|
|
26276
|
+
if (filter.task_id) {
|
|
26277
|
+
conditions.push("task_id = ?");
|
|
26278
|
+
params.push(filter.task_id);
|
|
26279
|
+
}
|
|
26280
|
+
if (filter.run_id) {
|
|
26281
|
+
conditions.push("run_id = ?");
|
|
26282
|
+
params.push(resolveTaskRunId(filter.run_id, d));
|
|
26283
|
+
}
|
|
26284
|
+
if (filter.status) {
|
|
26285
|
+
conditions.push("status = ?");
|
|
26286
|
+
params.push(normalizeStatus(filter.status));
|
|
26287
|
+
}
|
|
26288
|
+
if (filter.source) {
|
|
26289
|
+
conditions.push("source = ?");
|
|
26290
|
+
params.push(redactOptional(filter.source, 120));
|
|
26291
|
+
}
|
|
26292
|
+
const limit = Math.min(Math.max(Math.floor(filter.limit ?? 50), 1), 500);
|
|
26293
|
+
const rows = d.query(`SELECT * FROM task_findings WHERE ${conditions.join(" AND ")} ORDER BY updated_at DESC, created_at DESC LIMIT ?`).all(...[...params, limit]);
|
|
26294
|
+
return rows.map(rowToFinding);
|
|
26295
|
+
}
|
|
26296
|
+
function listCompactTaskFindings(filter = {}, db) {
|
|
26297
|
+
return listTaskFindings(filter, db).map(compactFinding);
|
|
26298
|
+
}
|
|
26299
|
+
function resolveMissingTaskFindings(input, db) {
|
|
26300
|
+
const d = db || getDatabase();
|
|
26301
|
+
assertTask(input.task_id, d);
|
|
26302
|
+
const timestamp3 = input.resolved_at || now();
|
|
26303
|
+
const status = normalizeResolutionStatus(input.status);
|
|
26304
|
+
const runId = resolveRunForTask(input.run_id, input.task_id, d);
|
|
26305
|
+
const present = new Set(input.fingerprints.map(normalizeFingerprint));
|
|
26306
|
+
const warnings = [];
|
|
26307
|
+
const conditions = ["task_id = ?", "status = 'open'"];
|
|
26308
|
+
const params = [input.task_id];
|
|
26309
|
+
if (input.source) {
|
|
26310
|
+
conditions.push("source = ?");
|
|
26311
|
+
params.push(redactOptional(input.source, 120));
|
|
26312
|
+
}
|
|
26313
|
+
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));
|
|
26314
|
+
const limit = Math.min(Math.max(Math.floor(input.limit ?? 50), 1), 200);
|
|
26315
|
+
const display = candidates.slice(0, limit);
|
|
26316
|
+
const omittedCount = Math.max(0, candidates.length - display.length);
|
|
26317
|
+
if (!input.apply) {
|
|
26318
|
+
return {
|
|
26319
|
+
schema_version: TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
|
|
26320
|
+
local_only: true,
|
|
26321
|
+
dry_run: true,
|
|
26322
|
+
processed_at: timestamp3,
|
|
26323
|
+
action: candidates.length > 0 ? "preview" : "noop",
|
|
26324
|
+
task_id: input.task_id,
|
|
26325
|
+
source: input.source ? redactOptional(input.source, 120) : null,
|
|
26326
|
+
run_id: runId,
|
|
26327
|
+
present_fingerprint_count: present.size,
|
|
26328
|
+
candidate_count: candidates.length,
|
|
26329
|
+
changed_count: 0,
|
|
26330
|
+
omitted_count: omittedCount,
|
|
26331
|
+
findings: display.map(compactFinding),
|
|
26332
|
+
warnings
|
|
26333
|
+
};
|
|
26334
|
+
}
|
|
26335
|
+
const metadataPatch = redactValue({
|
|
26336
|
+
resolved_by: {
|
|
26337
|
+
agent_id: input.agent_id ?? null,
|
|
26338
|
+
run_id: runId,
|
|
26339
|
+
reason: input.reason ? redactEvidenceText(input.reason) : "missing from latest loop finding set"
|
|
26340
|
+
}
|
|
26341
|
+
});
|
|
26342
|
+
const tx = d.transaction(() => {
|
|
26343
|
+
for (const finding2 of candidates) {
|
|
26344
|
+
d.run("UPDATE task_findings SET status = ?, resolved_at = ?, metadata = ?, updated_at = ? WHERE id = ? AND status = 'open'", [
|
|
26345
|
+
status,
|
|
26346
|
+
timestamp3,
|
|
26347
|
+
JSON.stringify({ ...finding2.metadata, ...metadataPatch }),
|
|
26348
|
+
timestamp3,
|
|
26349
|
+
finding2.id
|
|
26350
|
+
]);
|
|
26351
|
+
}
|
|
26352
|
+
});
|
|
26353
|
+
tx();
|
|
26354
|
+
const updated = candidates.map((finding2) => d.query("SELECT * FROM task_findings WHERE id = ?").get(finding2.id)).filter((row) => Boolean(row)).map(rowToFinding);
|
|
26355
|
+
const visibleUpdated = updated.slice(0, limit);
|
|
26356
|
+
return {
|
|
26357
|
+
schema_version: TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
|
|
26358
|
+
local_only: true,
|
|
26359
|
+
dry_run: false,
|
|
26360
|
+
processed_at: timestamp3,
|
|
26361
|
+
action: updated.length > 0 ? status : "noop",
|
|
26362
|
+
task_id: input.task_id,
|
|
26363
|
+
source: input.source ? redactOptional(input.source, 120) : null,
|
|
26364
|
+
run_id: runId,
|
|
26365
|
+
present_fingerprint_count: present.size,
|
|
26366
|
+
candidate_count: candidates.length,
|
|
26367
|
+
changed_count: updated.length,
|
|
26368
|
+
omitted_count: omittedCount,
|
|
26369
|
+
findings: visibleUpdated.map(compactFinding),
|
|
26370
|
+
warnings
|
|
26371
|
+
};
|
|
26372
|
+
}
|
|
26373
|
+
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;
|
|
26374
|
+
var init_findings = __esm(() => {
|
|
26375
|
+
init_redaction();
|
|
26376
|
+
init_types();
|
|
26377
|
+
init_database();
|
|
26378
|
+
init_tasks();
|
|
26379
|
+
init_task_runs();
|
|
26380
|
+
SEVERITIES = new Set(["low", "medium", "high", "critical"]);
|
|
26381
|
+
STATUSES = new Set(["open", "resolved", "ignored"]);
|
|
26382
|
+
});
|
|
26383
|
+
|
|
25487
26384
|
// src/lib/agent-run-dispatcher.ts
|
|
25488
26385
|
function dispatcherFromRun(run) {
|
|
25489
26386
|
const value = run.metadata["agent_run_dispatcher"];
|
|
@@ -26144,7 +27041,7 @@ function parseArray2(value) {
|
|
|
26144
27041
|
return [];
|
|
26145
27042
|
}
|
|
26146
27043
|
}
|
|
26147
|
-
function
|
|
27044
|
+
function parseObject3(value) {
|
|
26148
27045
|
try {
|
|
26149
27046
|
const parsed = JSON.parse(value || "{}");
|
|
26150
27047
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
@@ -26185,7 +27082,7 @@ function rowToKnowledgeRecord(row) {
|
|
|
26185
27082
|
agent_id: row.agent_id,
|
|
26186
27083
|
snapshot_id: row.snapshot_id,
|
|
26187
27084
|
tags: parseArray2(row.tags),
|
|
26188
|
-
metadata: redactValue(
|
|
27085
|
+
metadata: redactValue(parseObject3(row.metadata)),
|
|
26189
27086
|
created_at: row.created_at,
|
|
26190
27087
|
updated_at: row.updated_at
|
|
26191
27088
|
};
|
|
@@ -26404,7 +27301,7 @@ function parseArray3(value) {
|
|
|
26404
27301
|
return [];
|
|
26405
27302
|
}
|
|
26406
27303
|
}
|
|
26407
|
-
function
|
|
27304
|
+
function parseObject4(value) {
|
|
26408
27305
|
try {
|
|
26409
27306
|
const parsed = JSON.parse(value || "{}");
|
|
26410
27307
|
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
@@ -26453,7 +27350,7 @@ function rowToRisk(row) {
|
|
|
26453
27350
|
plan_id: row.plan_id,
|
|
26454
27351
|
task_id: row.task_id,
|
|
26455
27352
|
tags: parseArray3(row.tags),
|
|
26456
|
-
metadata: redactValue(
|
|
27353
|
+
metadata: redactValue(parseObject4(row.metadata)),
|
|
26457
27354
|
created_at: row.created_at,
|
|
26458
27355
|
updated_at: row.updated_at,
|
|
26459
27356
|
closed_at: row.closed_at
|
|
@@ -32295,6 +33192,38 @@ ${lines.join(`
|
|
|
32295
33192
|
}
|
|
32296
33193
|
});
|
|
32297
33194
|
}
|
|
33195
|
+
if (shouldRegisterTool("begin_task_run_transaction")) {
|
|
33196
|
+
server.tool("begin_task_run_transaction", "Preview or apply an idempotent local loop run transaction keyed by a stable loop/run id.", {
|
|
33197
|
+
task_id: exports_external.string().describe("Task ID"),
|
|
33198
|
+
key: exports_external.string().optional().describe("Stable idempotency key"),
|
|
33199
|
+
loop_id: exports_external.string().optional().describe("Loop identifier; used as key fallback"),
|
|
33200
|
+
loop_run_id: exports_external.string().optional().describe("Loop run identifier; used as key fallback"),
|
|
33201
|
+
agent_id: exports_external.string().optional().describe("Agent starting the run"),
|
|
33202
|
+
title: exports_external.string().optional().describe("Run title"),
|
|
33203
|
+
summary: exports_external.string().optional().describe("Run summary"),
|
|
33204
|
+
metadata: exports_external.record(exports_external.unknown()).optional().describe("Additional local metadata"),
|
|
33205
|
+
claim: exports_external.boolean().optional().describe("Claim/start the task before recording the run"),
|
|
33206
|
+
apply: exports_external.boolean().optional().describe("Apply the transaction; false or omitted is dry-run")
|
|
33207
|
+
}, async ({ task_id, key, loop_id, loop_run_id, agent_id, title, summary, metadata, claim, apply }) => {
|
|
33208
|
+
try {
|
|
33209
|
+
const result = beginTaskRunTransaction({
|
|
33210
|
+
task_id: resolveId(task_id),
|
|
33211
|
+
key,
|
|
33212
|
+
loop_id,
|
|
33213
|
+
loop_run_id,
|
|
33214
|
+
agent_id,
|
|
33215
|
+
title,
|
|
33216
|
+
summary,
|
|
33217
|
+
metadata,
|
|
33218
|
+
claim,
|
|
33219
|
+
apply
|
|
33220
|
+
});
|
|
33221
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
33222
|
+
} catch (e) {
|
|
33223
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
33224
|
+
}
|
|
33225
|
+
});
|
|
33226
|
+
}
|
|
32298
33227
|
if (shouldRegisterTool("list_task_runs")) {
|
|
32299
33228
|
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 }) => {
|
|
32300
33229
|
try {
|
|
@@ -32392,15 +33321,117 @@ ${lines.join(`
|
|
|
32392
33321
|
});
|
|
32393
33322
|
}
|
|
32394
33323
|
if (shouldRegisterTool("finish_task_run")) {
|
|
32395
|
-
server.tool("finish_task_run", "Finish a local run ledger entry as completed, failed, or cancelled.", {
|
|
32396
|
-
run_id: exports_external.string().describe("Run ID or prefix"),
|
|
32397
|
-
|
|
33324
|
+
server.tool("finish_task_run", "Finish a local run ledger entry as completed, failed, or cancelled. Supports idempotent lookup by key.", {
|
|
33325
|
+
run_id: exports_external.string().optional().describe("Run ID or prefix"),
|
|
33326
|
+
key: exports_external.string().optional().describe("Idempotency key when run_id is omitted"),
|
|
33327
|
+
task_id: exports_external.string().optional().describe("Task scope for key lookup"),
|
|
33328
|
+
status: exports_external.enum(["completed", "failed", "cancelled"]).optional().describe("Final run status"),
|
|
32398
33329
|
summary: exports_external.string().optional().describe("Final summary"),
|
|
32399
|
-
agent_id: exports_external.string().optional().describe("Agent finishing the run")
|
|
32400
|
-
|
|
33330
|
+
agent_id: exports_external.string().optional().describe("Agent finishing the run"),
|
|
33331
|
+
apply: exports_external.boolean().optional().describe("Apply the transaction; false previews without mutating")
|
|
33332
|
+
}, async ({ run_id, key, task_id, status, summary, agent_id, apply }) => {
|
|
33333
|
+
try {
|
|
33334
|
+
if (run_id && !key && apply === undefined) {
|
|
33335
|
+
const result2 = finishTaskRunTransaction({ run_id, status: status || "completed", summary, agent_id, apply: true });
|
|
33336
|
+
const run = result2.run ? getTaskRunLedger(result2.run.id).run : null;
|
|
33337
|
+
return { content: [{ type: "text", text: JSON.stringify(run, null, 2) }] };
|
|
33338
|
+
}
|
|
33339
|
+
const result = finishTaskRunTransaction({
|
|
33340
|
+
run_id,
|
|
33341
|
+
key,
|
|
33342
|
+
task_id: task_id ? resolveId(task_id) : undefined,
|
|
33343
|
+
status: status || "completed",
|
|
33344
|
+
summary,
|
|
33345
|
+
agent_id,
|
|
33346
|
+
apply: apply !== false
|
|
33347
|
+
});
|
|
33348
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
33349
|
+
} catch (e) {
|
|
33350
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
33351
|
+
}
|
|
33352
|
+
});
|
|
33353
|
+
}
|
|
33354
|
+
if (shouldRegisterTool("upsert_task_finding")) {
|
|
33355
|
+
server.tool("upsert_task_finding", "Preview or apply an idempotent local finding upsert scoped by task and fingerprint.", {
|
|
33356
|
+
task_id: exports_external.string().describe("Task ID"),
|
|
33357
|
+
fingerprint: exports_external.string().describe("Stable finding fingerprint"),
|
|
33358
|
+
title: exports_external.string().describe("Finding title"),
|
|
33359
|
+
severity: exports_external.enum(["low", "medium", "high", "critical"]).optional().describe("Finding severity"),
|
|
33360
|
+
status: exports_external.enum(["open", "resolved", "ignored"]).optional().describe("Finding status"),
|
|
33361
|
+
source: exports_external.string().optional().describe("Loop/tool source name"),
|
|
33362
|
+
summary: exports_external.string().optional().describe("Bounded finding summary"),
|
|
33363
|
+
artifact_path: exports_external.string().optional().describe("Local artifact path/reference; content is not read"),
|
|
33364
|
+
run_id: exports_external.string().optional().describe("Run ledger ID or prefix"),
|
|
33365
|
+
metadata: exports_external.record(exports_external.unknown()).optional().describe("Additional local metadata"),
|
|
33366
|
+
apply: exports_external.boolean().optional().describe("Apply the upsert; false or omitted is dry-run")
|
|
33367
|
+
}, async ({ task_id, fingerprint: fingerprint3, title, severity, status, source: source3, summary, artifact_path, run_id, metadata, apply }) => {
|
|
32401
33368
|
try {
|
|
32402
|
-
const
|
|
32403
|
-
|
|
33369
|
+
const result = upsertTaskFinding({
|
|
33370
|
+
task_id: resolveId(task_id),
|
|
33371
|
+
fingerprint: fingerprint3,
|
|
33372
|
+
title,
|
|
33373
|
+
severity,
|
|
33374
|
+
status,
|
|
33375
|
+
source: source3,
|
|
33376
|
+
summary,
|
|
33377
|
+
artifact_path,
|
|
33378
|
+
run_id,
|
|
33379
|
+
metadata,
|
|
33380
|
+
apply
|
|
33381
|
+
});
|
|
33382
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
33383
|
+
} catch (e) {
|
|
33384
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
33385
|
+
}
|
|
33386
|
+
});
|
|
33387
|
+
}
|
|
33388
|
+
if (shouldRegisterTool("list_task_findings")) {
|
|
33389
|
+
server.tool("list_task_findings", "List compact local findings with bounded output.", {
|
|
33390
|
+
task_id: exports_external.string().optional().describe("Filter by task"),
|
|
33391
|
+
run_id: exports_external.string().optional().describe("Filter by run"),
|
|
33392
|
+
status: exports_external.enum(["open", "resolved", "ignored"]).optional().describe("Filter by status"),
|
|
33393
|
+
source: exports_external.string().optional().describe("Filter by source"),
|
|
33394
|
+
limit: exports_external.number().optional().describe("Maximum findings to return")
|
|
33395
|
+
}, async ({ task_id, run_id, status, source: source3, limit }) => {
|
|
33396
|
+
try {
|
|
33397
|
+
const findings = listCompactTaskFindings({
|
|
33398
|
+
task_id: task_id ? resolveId(task_id) : undefined,
|
|
33399
|
+
run_id,
|
|
33400
|
+
status,
|
|
33401
|
+
source: source3,
|
|
33402
|
+
limit
|
|
33403
|
+
});
|
|
33404
|
+
return { content: [{ type: "text", text: JSON.stringify(findings) }] };
|
|
33405
|
+
} catch (e) {
|
|
33406
|
+
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
33407
|
+
}
|
|
33408
|
+
});
|
|
33409
|
+
}
|
|
33410
|
+
if (shouldRegisterTool("resolve_missing_task_findings")) {
|
|
33411
|
+
server.tool("resolve_missing_task_findings", "Resolve open findings absent from the latest loop finding set.", {
|
|
33412
|
+
task_id: exports_external.string().describe("Task ID"),
|
|
33413
|
+
fingerprints: exports_external.array(exports_external.string()).optional().describe("Fingerprints still present in the latest run"),
|
|
33414
|
+
source: exports_external.string().optional().describe("Only resolve findings from this source"),
|
|
33415
|
+
run_id: exports_external.string().optional().describe("Run ledger ID or prefix for audit metadata"),
|
|
33416
|
+
status: exports_external.enum(["resolved", "ignored"]).optional().describe("Resolution status"),
|
|
33417
|
+
agent_id: exports_external.string().optional().describe("Agent resolving findings"),
|
|
33418
|
+
reason: exports_external.string().optional().describe("Resolution reason"),
|
|
33419
|
+
limit: exports_external.number().optional().describe("Maximum findings returned"),
|
|
33420
|
+
apply: exports_external.boolean().optional().describe("Apply resolution; false or omitted is dry-run")
|
|
33421
|
+
}, async ({ task_id, fingerprints, source: source3, run_id, status, agent_id, reason, limit, apply }) => {
|
|
33422
|
+
try {
|
|
33423
|
+
const result = resolveMissingTaskFindings({
|
|
33424
|
+
task_id: resolveId(task_id),
|
|
33425
|
+
fingerprints: fingerprints || [],
|
|
33426
|
+
source: source3,
|
|
33427
|
+
run_id,
|
|
33428
|
+
status,
|
|
33429
|
+
agent_id,
|
|
33430
|
+
reason,
|
|
33431
|
+
limit,
|
|
33432
|
+
apply
|
|
33433
|
+
});
|
|
33434
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
32404
33435
|
} catch (e) {
|
|
32405
33436
|
return { content: [{ type: "text", text: formatError(e) }], isError: true };
|
|
32406
33437
|
}
|
|
@@ -32736,6 +33767,7 @@ var init_task_resources = __esm(() => {
|
|
|
32736
33767
|
init_agents();
|
|
32737
33768
|
init_task_commits();
|
|
32738
33769
|
init_task_runs();
|
|
33770
|
+
init_findings();
|
|
32739
33771
|
init_agent_run_dispatcher();
|
|
32740
33772
|
init_verification_providers();
|
|
32741
33773
|
init_release_notes();
|
|
@@ -37280,6 +38312,39 @@ async function handleCreateTask(req, ctx, json2, taskToSummary2) {
|
|
|
37280
38312
|
return json2({ error: e instanceof Error ? e.message : "Failed to create task" }, 500);
|
|
37281
38313
|
}
|
|
37282
38314
|
}
|
|
38315
|
+
async function handleUpsertTask(req, ctx, json2, taskToSummary2) {
|
|
38316
|
+
try {
|
|
38317
|
+
const body = await req.json();
|
|
38318
|
+
if (typeof body["fingerprint"] !== "string" || body["fingerprint"].trim() === "") {
|
|
38319
|
+
return json2({ error: "Missing 'fingerprint'" }, 400);
|
|
38320
|
+
}
|
|
38321
|
+
if (typeof body["title"] !== "string" || body["title"].trim() === "") {
|
|
38322
|
+
return json2({ error: "Missing 'title'" }, 400);
|
|
38323
|
+
}
|
|
38324
|
+
const metadata = body["metadata"] && typeof body["metadata"] === "object" && !Array.isArray(body["metadata"]) ? { ...body["metadata"] } : {};
|
|
38325
|
+
for (const key of ["expectation_id", "expectation_fingerprint", "evidence_paths", "origin_loop_id", "origin_run_id", "expected", "observed", "acceptance"]) {
|
|
38326
|
+
if (body[key] !== undefined)
|
|
38327
|
+
metadata[key] = body[key];
|
|
38328
|
+
}
|
|
38329
|
+
const result = upsertTaskByFingerprint({
|
|
38330
|
+
fingerprint: body["fingerprint"],
|
|
38331
|
+
title: body["title"],
|
|
38332
|
+
description: typeof body["description"] === "string" ? body["description"] : undefined,
|
|
38333
|
+
status: body["status"],
|
|
38334
|
+
priority: body["priority"],
|
|
38335
|
+
project_id: typeof body["project_id"] === "string" ? body["project_id"] : undefined,
|
|
38336
|
+
task_list_id: typeof body["task_list_id"] === "string" ? body["task_list_id"] : undefined,
|
|
38337
|
+
assigned_to: typeof body["assigned_to"] === "string" ? body["assigned_to"] : undefined,
|
|
38338
|
+
working_dir: typeof body["working_dir"] === "string" ? body["working_dir"] : undefined,
|
|
38339
|
+
tags: Array.isArray(body["tags"]) ? body["tags"].filter((tag) => typeof tag === "string") : undefined,
|
|
38340
|
+
metadata
|
|
38341
|
+
});
|
|
38342
|
+
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 });
|
|
38343
|
+
return json2({ created: result.created, task: taskToSummary2(result.task) }, result.created ? 201 : 200);
|
|
38344
|
+
} catch (e) {
|
|
38345
|
+
return json2({ error: e instanceof Error ? e.message : "Failed to upsert task" }, 500);
|
|
38346
|
+
}
|
|
38347
|
+
}
|
|
37283
38348
|
function handleTasksExport(_req, url, _ctx, _json, taskToSummary2) {
|
|
37284
38349
|
const format = url.searchParams.get("format") || "json";
|
|
37285
38350
|
const status = url.searchParams.get("status") || undefined;
|
|
@@ -37960,8 +39025,10 @@ function taskToSummary(task2, fields) {
|
|
|
37960
39025
|
task_list_id: task2.task_list_id,
|
|
37961
39026
|
agent_id: task2.agent_id,
|
|
37962
39027
|
assigned_to: task2.assigned_to,
|
|
39028
|
+
working_dir: task2.working_dir,
|
|
37963
39029
|
locked_by: task2.locked_by,
|
|
37964
39030
|
tags: task2.tags,
|
|
39031
|
+
metadata: task2.metadata,
|
|
37965
39032
|
version: task2.version,
|
|
37966
39033
|
created_at: task2.created_at,
|
|
37967
39034
|
updated_at: task2.updated_at,
|
|
@@ -38103,6 +39170,9 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
38103
39170
|
if (path === "/api/tasks" && method === "POST") {
|
|
38104
39171
|
return handleCreateTask(req, ctx, jsonWithCors, taskToSummary);
|
|
38105
39172
|
}
|
|
39173
|
+
if (path === "/api/tasks/upsert" && method === "POST") {
|
|
39174
|
+
return handleUpsertTask(req, ctx, jsonWithCors, taskToSummary);
|
|
39175
|
+
}
|
|
38106
39176
|
if (path === "/api/tasks/export" && method === "GET") {
|
|
38107
39177
|
return handleTasksExport(req, url, ctx, jsonWithCors, taskToSummary);
|
|
38108
39178
|
}
|
|
@@ -38298,7 +39368,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
38298
39368
|
} catch {}
|
|
38299
39369
|
}
|
|
38300
39370
|
}
|
|
38301
|
-
var MIME_TYPES, SECURITY_HEADERS, rateLimitMap, RATE_LIMIT_WINDOW_MS = 60000, RATE_LIMIT_MAX
|
|
39371
|
+
var MIME_TYPES, SECURITY_HEADERS, rateLimitMap, RATE_LIMIT_WINDOW_MS = 60000, RATE_LIMIT_MAX;
|
|
38302
39372
|
var init_serve = __esm(() => {
|
|
38303
39373
|
init_database();
|
|
38304
39374
|
init_api_keys();
|
|
@@ -38323,6 +39393,7 @@ var init_serve = __esm(() => {
|
|
|
38323
39393
|
"Permissions-Policy": "camera=, microphone=, geolocation="
|
|
38324
39394
|
};
|
|
38325
39395
|
rateLimitMap = new Map;
|
|
39396
|
+
RATE_LIMIT_MAX = Number.parseInt(process.env["TODOS_RATE_LIMIT_MAX"] || "120", 10);
|
|
38326
39397
|
});
|
|
38327
39398
|
|
|
38328
39399
|
// src/mcp/index.ts
|