@hasna/todos 0.11.58 → 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 +1420 -187
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +502 -4
- 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 +870 -5
- 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 +984 -17
- 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 +502 -4
- package/dist/release-provenance.json +3 -3
- package/dist/server/index.js +984 -17
- 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 +375 -1
- package/dist/types/index.d.ts +11 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/contracts.js
CHANGED
|
@@ -1139,6 +1139,49 @@ var init_migrations = __esm(() => {
|
|
|
1139
1139
|
CREATE INDEX IF NOT EXISTS idx_local_retrospectives_plan ON local_retrospectives(plan_id);
|
|
1140
1140
|
CREATE INDEX IF NOT EXISTS idx_local_retrospectives_agent ON local_retrospectives(agent_id);
|
|
1141
1141
|
INSERT OR IGNORE INTO _migrations (id) VALUES (61);
|
|
1142
|
+
`,
|
|
1143
|
+
`
|
|
1144
|
+
CREATE TABLE IF NOT EXISTS task_run_transactions (
|
|
1145
|
+
id TEXT PRIMARY KEY,
|
|
1146
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1147
|
+
run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
|
|
1148
|
+
key TEXT NOT NULL,
|
|
1149
|
+
loop_id TEXT,
|
|
1150
|
+
loop_run_id TEXT,
|
|
1151
|
+
metadata TEXT DEFAULT '{}',
|
|
1152
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1153
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1154
|
+
UNIQUE(task_id, key)
|
|
1155
|
+
);
|
|
1156
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_transactions_task_key ON task_run_transactions(task_id, key);
|
|
1157
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_transactions_key ON task_run_transactions(key);
|
|
1158
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_transactions_run ON task_run_transactions(run_id);
|
|
1159
|
+
|
|
1160
|
+
CREATE TABLE IF NOT EXISTS task_findings (
|
|
1161
|
+
id TEXT PRIMARY KEY,
|
|
1162
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1163
|
+
run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
|
|
1164
|
+
fingerprint TEXT NOT NULL,
|
|
1165
|
+
title TEXT NOT NULL,
|
|
1166
|
+
severity TEXT NOT NULL DEFAULT 'medium' CHECK(severity IN ('low', 'medium', 'high', 'critical')),
|
|
1167
|
+
status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open', 'resolved', 'ignored')),
|
|
1168
|
+
source TEXT,
|
|
1169
|
+
summary TEXT,
|
|
1170
|
+
artifact_path TEXT,
|
|
1171
|
+
metadata TEXT DEFAULT '{}',
|
|
1172
|
+
first_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1173
|
+
last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1174
|
+
resolved_at TEXT,
|
|
1175
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1176
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1177
|
+
UNIQUE(task_id, fingerprint)
|
|
1178
|
+
);
|
|
1179
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_task ON task_findings(task_id);
|
|
1180
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_run ON task_findings(run_id);
|
|
1181
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_status ON task_findings(status);
|
|
1182
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_source ON task_findings(source);
|
|
1183
|
+
CREATE INDEX IF NOT EXISTS idx_task_findings_fingerprint ON task_findings(fingerprint);
|
|
1184
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (62);
|
|
1142
1185
|
`
|
|
1143
1186
|
];
|
|
1144
1187
|
});
|
|
@@ -1576,6 +1619,47 @@ function ensureSchema(db) {
|
|
|
1576
1619
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_run ON task_run_artifacts(run_id)");
|
|
1577
1620
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_task ON task_run_artifacts(task_id)");
|
|
1578
1621
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_path ON task_run_artifacts(path)");
|
|
1622
|
+
ensureTable("task_run_transactions", `
|
|
1623
|
+
CREATE TABLE task_run_transactions (
|
|
1624
|
+
id TEXT PRIMARY KEY,
|
|
1625
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1626
|
+
run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
|
|
1627
|
+
key TEXT NOT NULL,
|
|
1628
|
+
loop_id TEXT,
|
|
1629
|
+
loop_run_id TEXT,
|
|
1630
|
+
metadata TEXT DEFAULT '{}',
|
|
1631
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1632
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1633
|
+
UNIQUE(task_id, key)
|
|
1634
|
+
)`);
|
|
1635
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_transactions_task_key ON task_run_transactions(task_id, key)");
|
|
1636
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_transactions_key ON task_run_transactions(key)");
|
|
1637
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_transactions_run ON task_run_transactions(run_id)");
|
|
1638
|
+
ensureTable("task_findings", `
|
|
1639
|
+
CREATE TABLE task_findings (
|
|
1640
|
+
id TEXT PRIMARY KEY,
|
|
1641
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1642
|
+
run_id TEXT REFERENCES task_runs(id) ON DELETE SET NULL,
|
|
1643
|
+
fingerprint TEXT NOT NULL,
|
|
1644
|
+
title TEXT NOT NULL,
|
|
1645
|
+
severity TEXT NOT NULL DEFAULT 'medium' CHECK(severity IN ('low', 'medium', 'high', 'critical')),
|
|
1646
|
+
status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open', 'resolved', 'ignored')),
|
|
1647
|
+
source TEXT,
|
|
1648
|
+
summary TEXT,
|
|
1649
|
+
artifact_path TEXT,
|
|
1650
|
+
metadata TEXT DEFAULT '{}',
|
|
1651
|
+
first_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1652
|
+
last_seen_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1653
|
+
resolved_at TEXT,
|
|
1654
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1655
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1656
|
+
UNIQUE(task_id, fingerprint)
|
|
1657
|
+
)`);
|
|
1658
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_task ON task_findings(task_id)");
|
|
1659
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_run ON task_findings(run_id)");
|
|
1660
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_status ON task_findings(status)");
|
|
1661
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_source ON task_findings(source)");
|
|
1662
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_findings_fingerprint ON task_findings(fingerprint)");
|
|
1579
1663
|
ensureTable("inbox_items", `
|
|
1580
1664
|
CREATE TABLE inbox_items (
|
|
1581
1665
|
id TEXT PRIMARY KEY,
|
|
@@ -3576,6 +3660,7 @@ var MCP_TOOL_GROUPS = {
|
|
|
3576
3660
|
"add_task_run_event",
|
|
3577
3661
|
"add_task_run_file",
|
|
3578
3662
|
"acknowledge_handoff",
|
|
3663
|
+
"begin_task_run_transaction",
|
|
3579
3664
|
"build_local_report",
|
|
3580
3665
|
"cancel_agent_run_dispatch",
|
|
3581
3666
|
"finish_task_run",
|
|
@@ -3623,6 +3708,7 @@ var MCP_TOOL_GROUPS = {
|
|
|
3623
3708
|
"list_local_snapshots",
|
|
3624
3709
|
"list_retrospectives",
|
|
3625
3710
|
"list_risks",
|
|
3711
|
+
"list_task_findings",
|
|
3626
3712
|
"list_task_runs",
|
|
3627
3713
|
"list_verification_providers",
|
|
3628
3714
|
"merge_duplicate_task",
|
|
@@ -3631,6 +3717,7 @@ var MCP_TOOL_GROUPS = {
|
|
|
3631
3717
|
"remove_review_routing_rule",
|
|
3632
3718
|
"restore_local_backup",
|
|
3633
3719
|
"retry_agent_run_dispatch",
|
|
3720
|
+
"resolve_missing_task_findings",
|
|
3634
3721
|
"resolve_mentions",
|
|
3635
3722
|
"run_next_agent_dispatch",
|
|
3636
3723
|
"search_knowledge_records",
|
|
@@ -3673,9 +3760,17 @@ var MCP_TOOL_GROUPS = {
|
|
|
3673
3760
|
"unlock_file",
|
|
3674
3761
|
"unwatch_task",
|
|
3675
3762
|
"update_comment",
|
|
3763
|
+
"upsert_task_finding",
|
|
3676
3764
|
"update_risk",
|
|
3677
3765
|
"watch_task"
|
|
3678
3766
|
],
|
|
3767
|
+
loops: [
|
|
3768
|
+
"begin_task_run_transaction",
|
|
3769
|
+
"finish_task_run",
|
|
3770
|
+
"list_task_findings",
|
|
3771
|
+
"resolve_missing_task_findings",
|
|
3772
|
+
"upsert_task_finding"
|
|
3773
|
+
],
|
|
3679
3774
|
agents: [
|
|
3680
3775
|
"auto_assign_task",
|
|
3681
3776
|
"delete_agent",
|
|
@@ -3757,7 +3852,7 @@ var MCP_TOOL_GROUPS = {
|
|
|
3757
3852
|
maintenance: ["extract_todos", "get_sla_breaches", "notify_upcoming_deadlines", "run_doctor", "score_task", "watch_source_todos"]
|
|
3758
3853
|
};
|
|
3759
3854
|
var MCP_PROFILE_GROUPS = {
|
|
3760
|
-
minimal: ["core"],
|
|
3855
|
+
minimal: ["core", "loops"],
|
|
3761
3856
|
core: ["core"],
|
|
3762
3857
|
standard: ["core", "tasks", "projects", "resources", "agents", "metadata"],
|
|
3763
3858
|
agent: ["core", "tasks", "projects", "resources"],
|
|
@@ -4830,6 +4925,92 @@ var TODOS_JSON_CONTRACTS = [
|
|
|
4830
4925
|
},
|
|
4831
4926
|
optional: {}
|
|
4832
4927
|
}),
|
|
4928
|
+
contract({
|
|
4929
|
+
id: "loop_run_transaction",
|
|
4930
|
+
name: "Loop Run Transaction",
|
|
4931
|
+
description: "Compact local result for idempotent loop run begin/finish transactions.",
|
|
4932
|
+
surfaces: ["cli", "mcp", "sdk"],
|
|
4933
|
+
stability: "stable",
|
|
4934
|
+
required: {
|
|
4935
|
+
schema_version: field("string", "Result schema version."),
|
|
4936
|
+
local_only: field("boolean", "Always true; loop run transactions use local state."),
|
|
4937
|
+
dry_run: field("boolean", "True when no run ledger mutation was applied."),
|
|
4938
|
+
processed_at: isoDateField,
|
|
4939
|
+
action: field("string", "preview, created, matched, finished, or conflict."),
|
|
4940
|
+
key: field("string", "Stable idempotency key used to dedupe the transaction."),
|
|
4941
|
+
run: field(["object", "null"], "Compact run summary or null for create previews.", true),
|
|
4942
|
+
warnings: field("array", "Non-fatal warnings such as terminal-status conflicts."),
|
|
4943
|
+
commands: field("array", "Follow-up CLI commands for agents and operators.")
|
|
4944
|
+
},
|
|
4945
|
+
optional: {}
|
|
4946
|
+
}),
|
|
4947
|
+
contract({
|
|
4948
|
+
id: "task_finding",
|
|
4949
|
+
name: "Task Finding",
|
|
4950
|
+
description: "Compact local finding record deduped by task and fingerprint.",
|
|
4951
|
+
surfaces: ["cli", "mcp", "sdk"],
|
|
4952
|
+
stability: "stable",
|
|
4953
|
+
required: {
|
|
4954
|
+
schema_version: field("string", "Finding schema version."),
|
|
4955
|
+
id: idField,
|
|
4956
|
+
task_id: idField,
|
|
4957
|
+
run_id: field(["string", "null"], "Optional run ledger ID.", true),
|
|
4958
|
+
fingerprint: field("string", "Stable finding fingerprint scoped to the task."),
|
|
4959
|
+
title: field("string", "Short redacted finding title."),
|
|
4960
|
+
severity: field("string", "low, medium, high, or critical."),
|
|
4961
|
+
status: field("string", "open, resolved, or ignored."),
|
|
4962
|
+
source: field(["string", "null"], "Optional loop/tool source.", true),
|
|
4963
|
+
summary: field(["string", "null"], "Bounded redacted finding summary.", true),
|
|
4964
|
+
artifact_path: field(["string", "null"], "Local artifact path/reference; raw content is not included.", true),
|
|
4965
|
+
first_seen_at: isoDateField,
|
|
4966
|
+
last_seen_at: isoDateField,
|
|
4967
|
+
resolved_at: field(["string", "null"], "Resolution timestamp when closed.", true),
|
|
4968
|
+
metadata_keys: field("array", "Sorted metadata keys; metadata values are intentionally omitted in compact output.")
|
|
4969
|
+
},
|
|
4970
|
+
optional: {}
|
|
4971
|
+
}),
|
|
4972
|
+
contract({
|
|
4973
|
+
id: "task_finding_upsert",
|
|
4974
|
+
name: "Task Finding Upsert Result",
|
|
4975
|
+
description: "Local-only dry-run or applied result from idempotently upserting a task finding.",
|
|
4976
|
+
surfaces: ["cli", "mcp", "sdk"],
|
|
4977
|
+
stability: "stable",
|
|
4978
|
+
required: {
|
|
4979
|
+
schema_version: field("string", "Result schema version."),
|
|
4980
|
+
local_only: field("boolean", "Always true; finding upserts use local state."),
|
|
4981
|
+
dry_run: field("boolean", "True when no finding row was created or updated."),
|
|
4982
|
+
processed_at: isoDateField,
|
|
4983
|
+
action: field("string", "preview, created, matched, updated, or reopened."),
|
|
4984
|
+
fingerprint: field("string", "Normalized finding fingerprint."),
|
|
4985
|
+
finding: field(["object", "null"], "Compact finding summary or null for create previews.", true),
|
|
4986
|
+
warnings: field("array", "Non-fatal warnings.")
|
|
4987
|
+
},
|
|
4988
|
+
optional: {}
|
|
4989
|
+
}),
|
|
4990
|
+
contract({
|
|
4991
|
+
id: "task_finding_resolve_missing",
|
|
4992
|
+
name: "Task Finding Resolve Missing Result",
|
|
4993
|
+
description: "Local-only dry-run or applied result from resolving open findings absent from the latest loop finding set.",
|
|
4994
|
+
surfaces: ["cli", "mcp", "sdk"],
|
|
4995
|
+
stability: "stable",
|
|
4996
|
+
required: {
|
|
4997
|
+
schema_version: field("string", "Result schema version."),
|
|
4998
|
+
local_only: field("boolean", "Always true; finding resolution uses local state."),
|
|
4999
|
+
dry_run: field("boolean", "True when no finding rows were changed."),
|
|
5000
|
+
processed_at: isoDateField,
|
|
5001
|
+
action: field("string", "preview, resolved, ignored, or noop."),
|
|
5002
|
+
task_id: idField,
|
|
5003
|
+
source: field(["string", "null"], "Optional source scope.", true),
|
|
5004
|
+
run_id: field(["string", "null"], "Optional run ledger ID used for audit metadata.", true),
|
|
5005
|
+
present_fingerprint_count: field("integer", "Number of fingerprints supplied as still present."),
|
|
5006
|
+
candidate_count: field("integer", "Open findings missing from the supplied set."),
|
|
5007
|
+
changed_count: field("integer", "Rows resolved or ignored by the applied transaction."),
|
|
5008
|
+
omitted_count: field("integer", "Matching findings omitted from bounded output."),
|
|
5009
|
+
findings: field("array", "Bounded compact finding summaries."),
|
|
5010
|
+
warnings: field("array", "Non-fatal warnings.")
|
|
5011
|
+
},
|
|
5012
|
+
optional: {}
|
|
5013
|
+
}),
|
|
4833
5014
|
contract({
|
|
4834
5015
|
id: "verification_provider",
|
|
4835
5016
|
name: "Verification Provider",
|
|
@@ -6535,6 +6716,7 @@ var LOCAL_EVENT_TYPES = [
|
|
|
6535
6716
|
"task.blocked",
|
|
6536
6717
|
"task.started",
|
|
6537
6718
|
"task.completed",
|
|
6719
|
+
"task.updated",
|
|
6538
6720
|
"task.due",
|
|
6539
6721
|
"task.due_soon",
|
|
6540
6722
|
"task.failed",
|
|
@@ -7873,6 +8055,17 @@ function replaceTaskTags(taskId, tags, db) {
|
|
|
7873
8055
|
db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
|
|
7874
8056
|
insertTaskTags(taskId, tags, db);
|
|
7875
8057
|
}
|
|
8058
|
+
function addMetadataConditions(metadata, conditions, params) {
|
|
8059
|
+
if (!metadata)
|
|
8060
|
+
return;
|
|
8061
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
8062
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(key)) {
|
|
8063
|
+
throw new Error(`Invalid metadata filter key: ${key}`);
|
|
8064
|
+
}
|
|
8065
|
+
conditions.push(`json_extract(metadata, '$."${key}"') = ?`);
|
|
8066
|
+
params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
|
|
8067
|
+
}
|
|
8068
|
+
}
|
|
7876
8069
|
function createTask(input, db) {
|
|
7877
8070
|
const d = db || getDatabase();
|
|
7878
8071
|
const timestamp = now();
|
|
@@ -8055,6 +8248,7 @@ function listTasks(filter = {}, db) {
|
|
|
8055
8248
|
params.push(filter.task_type);
|
|
8056
8249
|
}
|
|
8057
8250
|
}
|
|
8251
|
+
addMetadataConditions(filter.metadata, conditions, params);
|
|
8058
8252
|
const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
|
|
8059
8253
|
if (filter.cursor) {
|
|
8060
8254
|
try {
|
|
@@ -8079,6 +8273,54 @@ function listTasks(filter = {}, db) {
|
|
|
8079
8273
|
const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC${limitClause}`).all(...params);
|
|
8080
8274
|
return rows.map(rowToTask);
|
|
8081
8275
|
}
|
|
8276
|
+
function getTaskByFingerprint(fingerprint, db) {
|
|
8277
|
+
const tasks = listTasks({ metadata: { fingerprint }, limit: 1 }, db);
|
|
8278
|
+
return tasks[0] ?? null;
|
|
8279
|
+
}
|
|
8280
|
+
function mergeTaskMetadata(current, next, fingerprint) {
|
|
8281
|
+
return {
|
|
8282
|
+
...current,
|
|
8283
|
+
...next ?? {},
|
|
8284
|
+
fingerprint
|
|
8285
|
+
};
|
|
8286
|
+
}
|
|
8287
|
+
function upsertTaskByFingerprint(input, db) {
|
|
8288
|
+
const d = db || getDatabase();
|
|
8289
|
+
const fingerprint = input.fingerprint.trim();
|
|
8290
|
+
if (!fingerprint)
|
|
8291
|
+
throw new Error("fingerprint is required");
|
|
8292
|
+
const existing = getTaskByFingerprint(fingerprint, d);
|
|
8293
|
+
const metadata = mergeTaskMetadata(existing?.metadata ?? {}, input.metadata, fingerprint);
|
|
8294
|
+
if (!existing) {
|
|
8295
|
+
const task2 = createTask({ ...input, metadata }, d);
|
|
8296
|
+
return { task: task2, created: true };
|
|
8297
|
+
}
|
|
8298
|
+
const task = updateTask(existing.id, {
|
|
8299
|
+
version: existing.version,
|
|
8300
|
+
title: input.title,
|
|
8301
|
+
description: input.description,
|
|
8302
|
+
status: input.status,
|
|
8303
|
+
priority: input.priority,
|
|
8304
|
+
project_id: input.project_id,
|
|
8305
|
+
assigned_to: input.assigned_to,
|
|
8306
|
+
working_dir: input.working_dir,
|
|
8307
|
+
plan_id: input.plan_id,
|
|
8308
|
+
task_list_id: input.task_list_id,
|
|
8309
|
+
tags: input.tags,
|
|
8310
|
+
metadata,
|
|
8311
|
+
due_at: input.due_at,
|
|
8312
|
+
estimated_minutes: input.estimated_minutes,
|
|
8313
|
+
sla_minutes: input.sla_minutes,
|
|
8314
|
+
confidence: input.confidence,
|
|
8315
|
+
retry_count: input.retry_count,
|
|
8316
|
+
max_retries: input.max_retries,
|
|
8317
|
+
retry_after: input.retry_after,
|
|
8318
|
+
requires_approval: input.requires_approval,
|
|
8319
|
+
recurrence_rule: input.recurrence_rule,
|
|
8320
|
+
task_type: input.task_type
|
|
8321
|
+
}, d);
|
|
8322
|
+
return { task, created: false };
|
|
8323
|
+
}
|
|
8082
8324
|
function countTasks(filter = {}, db) {
|
|
8083
8325
|
const d = db || getDatabase();
|
|
8084
8326
|
const conditions = [];
|
|
@@ -8142,6 +8384,7 @@ function countTasks(filter = {}, db) {
|
|
|
8142
8384
|
conditions.push("task_list_id = ?");
|
|
8143
8385
|
params.push(filter.task_list_id);
|
|
8144
8386
|
}
|
|
8387
|
+
addMetadataConditions(filter.metadata, conditions, params);
|
|
8145
8388
|
if (!filter.include_archived) {
|
|
8146
8389
|
conditions.push("archived_at IS NULL");
|
|
8147
8390
|
}
|
|
@@ -8192,6 +8435,10 @@ function updateTask(id, input, db) {
|
|
|
8192
8435
|
sets.push("assigned_to = ?");
|
|
8193
8436
|
params.push(input.assigned_to);
|
|
8194
8437
|
}
|
|
8438
|
+
if (input.working_dir !== undefined) {
|
|
8439
|
+
sets.push("working_dir = ?");
|
|
8440
|
+
params.push(input.working_dir);
|
|
8441
|
+
}
|
|
8195
8442
|
if (input.tags !== undefined) {
|
|
8196
8443
|
sets.push("tags = ?");
|
|
8197
8444
|
params.push(JSON.stringify(input.tags));
|
|
@@ -8280,6 +8527,8 @@ function updateTask(id, input, db) {
|
|
|
8280
8527
|
logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
|
|
8281
8528
|
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
|
|
8282
8529
|
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
8530
|
+
if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
|
|
8531
|
+
logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
|
|
8283
8532
|
if (input.approved_by !== undefined)
|
|
8284
8533
|
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
8285
8534
|
const updatedTask = {
|
|
@@ -8315,6 +8564,10 @@ function updateTask(id, input, db) {
|
|
|
8315
8564
|
if (input.approved_by !== undefined) {
|
|
8316
8565
|
emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
|
|
8317
8566
|
}
|
|
8567
|
+
const updatePayload = taskEventData(updatedTask);
|
|
8568
|
+
dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
|
|
8569
|
+
emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload });
|
|
8570
|
+
emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask });
|
|
8318
8571
|
return updatedTask;
|
|
8319
8572
|
}
|
|
8320
8573
|
function deleteTask(id, db) {
|
|
@@ -11010,6 +11263,7 @@ function getTaskTraceability(taskId, db) {
|
|
|
11010
11263
|
|
|
11011
11264
|
// src/db/task-runs.ts
|
|
11012
11265
|
init_redaction();
|
|
11266
|
+
var LOOP_RUN_TRANSACTION_SCHEMA_VERSION = "todos.loop_run_transaction.v1";
|
|
11013
11267
|
function parseObject(value) {
|
|
11014
11268
|
if (!value)
|
|
11015
11269
|
return {};
|
|
@@ -11032,6 +11286,72 @@ function rowToArtifact(row) {
|
|
|
11032
11286
|
function getRunRow(runId, db) {
|
|
11033
11287
|
return db.query("SELECT * FROM task_runs WHERE id = ?").get(runId);
|
|
11034
11288
|
}
|
|
11289
|
+
function normalizeTransactionKey(input) {
|
|
11290
|
+
const key = (input.key || input.loop_run_id || input.loop_id || "").trim();
|
|
11291
|
+
if (!key)
|
|
11292
|
+
throw new Error("idempotent run transactions require --key, --loop-run-id, or --loop-id");
|
|
11293
|
+
return key.toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 240);
|
|
11294
|
+
}
|
|
11295
|
+
function loopTransactionMetadata(record) {
|
|
11296
|
+
const value = record.metadata["loop_transaction"];
|
|
11297
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
11298
|
+
}
|
|
11299
|
+
function runKey(record) {
|
|
11300
|
+
const tx = loopTransactionMetadata(record);
|
|
11301
|
+
const key = tx["idempotency_key"] ?? record.metadata["idempotency_key"];
|
|
11302
|
+
return typeof key === "string" ? key : null;
|
|
11303
|
+
}
|
|
11304
|
+
function loopId(record) {
|
|
11305
|
+
const tx = loopTransactionMetadata(record);
|
|
11306
|
+
const value = tx["loop_id"] ?? record.metadata["loop_id"];
|
|
11307
|
+
return typeof value === "string" ? value : null;
|
|
11308
|
+
}
|
|
11309
|
+
function loopRunId(record) {
|
|
11310
|
+
const tx = loopTransactionMetadata(record);
|
|
11311
|
+
const value = tx["loop_run_id"] ?? record.metadata["loop_run_id"];
|
|
11312
|
+
return typeof value === "string" ? value : null;
|
|
11313
|
+
}
|
|
11314
|
+
function getTaskRunTransactionByKey(key, taskId, db) {
|
|
11315
|
+
if (taskId) {
|
|
11316
|
+
return db.query("SELECT * FROM task_run_transactions WHERE task_id = ? AND key = ?").get(taskId, key);
|
|
11317
|
+
}
|
|
11318
|
+
const rows = db.query("SELECT * FROM task_run_transactions WHERE key = ? ORDER BY created_at DESC LIMIT 2").all(key);
|
|
11319
|
+
if (rows.length > 1)
|
|
11320
|
+
throw new Error(`Run transaction key is ambiguous across tasks: ${key}. Pass task_id.`);
|
|
11321
|
+
return rows[0] ?? null;
|
|
11322
|
+
}
|
|
11323
|
+
function summarizeTaskRun(run) {
|
|
11324
|
+
return {
|
|
11325
|
+
id: run.id,
|
|
11326
|
+
task_id: run.task_id,
|
|
11327
|
+
agent_id: run.agent_id,
|
|
11328
|
+
title: run.title,
|
|
11329
|
+
status: run.status,
|
|
11330
|
+
summary: run.summary,
|
|
11331
|
+
idempotency_key: runKey(run),
|
|
11332
|
+
loop_id: loopId(run),
|
|
11333
|
+
loop_run_id: loopRunId(run),
|
|
11334
|
+
metadata_keys: Object.keys(run.metadata).sort(),
|
|
11335
|
+
started_at: run.started_at,
|
|
11336
|
+
completed_at: run.completed_at,
|
|
11337
|
+
updated_at: run.updated_at
|
|
11338
|
+
};
|
|
11339
|
+
}
|
|
11340
|
+
function findTaskRunByTransactionKey(key, taskId, db) {
|
|
11341
|
+
const d = db || getDatabase();
|
|
11342
|
+
const normalized = normalizeTransactionKey({ key });
|
|
11343
|
+
const transaction = getTaskRunTransactionByKey(normalized, taskId, d);
|
|
11344
|
+
if (transaction?.run_id)
|
|
11345
|
+
return getTaskRun(transaction.run_id, d);
|
|
11346
|
+
return null;
|
|
11347
|
+
}
|
|
11348
|
+
function loopRunCommands(run, key) {
|
|
11349
|
+
return [
|
|
11350
|
+
run ? `todos runs show ${run.id.slice(0, 8)}` : "todos runs list",
|
|
11351
|
+
run ? `todos findings list --task ${run.task_id.slice(0, 8)} --json` : "todos findings list --json",
|
|
11352
|
+
`todos runs begin <task-id> --key ${key} --apply --json`
|
|
11353
|
+
];
|
|
11354
|
+
}
|
|
11035
11355
|
function resolveTaskRunId(idOrPrefix, db) {
|
|
11036
11356
|
const d = db || getDatabase();
|
|
11037
11357
|
const rows = d.query("SELECT id FROM task_runs WHERE id = ? OR id LIKE ? ORDER BY created_at DESC LIMIT 2").all(idOrPrefix, `${idOrPrefix}%`);
|
|
@@ -11050,7 +11370,7 @@ function startTaskRun(input, db) {
|
|
|
11050
11370
|
const d = db || getDatabase();
|
|
11051
11371
|
if (!getTask(input.task_id, d))
|
|
11052
11372
|
throw new TaskNotFoundError(input.task_id);
|
|
11053
|
-
const id = uuid();
|
|
11373
|
+
const id = input.id ?? uuid();
|
|
11054
11374
|
const timestamp = input.started_at || now();
|
|
11055
11375
|
if (input.claim && input.agent_id) {
|
|
11056
11376
|
startTask(input.task_id, input.agent_id, d);
|
|
@@ -11088,6 +11408,97 @@ function startTaskRun(input, db) {
|
|
|
11088
11408
|
emitLocalEventHooksQuiet({ type: "run.started", payload: { id: run.id, task_id: run.task_id, agent_id: run.agent_id, title: run.title } });
|
|
11089
11409
|
return run;
|
|
11090
11410
|
}
|
|
11411
|
+
function beginTaskRunTransaction(input, db) {
|
|
11412
|
+
const d = db || getDatabase();
|
|
11413
|
+
if (!getTask(input.task_id, d))
|
|
11414
|
+
throw new TaskNotFoundError(input.task_id);
|
|
11415
|
+
const timestamp = input.started_at || now();
|
|
11416
|
+
const key = normalizeTransactionKey(input);
|
|
11417
|
+
const existing = findTaskRunByTransactionKey(key, input.task_id, d);
|
|
11418
|
+
const dryRun = !input.apply;
|
|
11419
|
+
if (existing) {
|
|
11420
|
+
return {
|
|
11421
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
11422
|
+
local_only: true,
|
|
11423
|
+
dry_run: dryRun,
|
|
11424
|
+
processed_at: timestamp,
|
|
11425
|
+
action: "matched",
|
|
11426
|
+
key,
|
|
11427
|
+
run: summarizeTaskRun(existing),
|
|
11428
|
+
warnings: existing.status === "running" ? [] : [`matched ${existing.status} run`],
|
|
11429
|
+
commands: loopRunCommands(existing, key)
|
|
11430
|
+
};
|
|
11431
|
+
}
|
|
11432
|
+
if (dryRun) {
|
|
11433
|
+
return {
|
|
11434
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
11435
|
+
local_only: true,
|
|
11436
|
+
dry_run: true,
|
|
11437
|
+
processed_at: timestamp,
|
|
11438
|
+
action: "preview",
|
|
11439
|
+
key,
|
|
11440
|
+
run: null,
|
|
11441
|
+
warnings: [],
|
|
11442
|
+
commands: loopRunCommands(null, key)
|
|
11443
|
+
};
|
|
11444
|
+
}
|
|
11445
|
+
const metadata = redactValue({
|
|
11446
|
+
...input.metadata || {},
|
|
11447
|
+
loop_transaction: {
|
|
11448
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
11449
|
+
idempotency_key: key,
|
|
11450
|
+
loop_id: input.loop_id ?? null,
|
|
11451
|
+
loop_run_id: input.loop_run_id ?? null,
|
|
11452
|
+
first_seen_at: timestamp
|
|
11453
|
+
},
|
|
11454
|
+
idempotency_key: key
|
|
11455
|
+
});
|
|
11456
|
+
const created = d.transaction(() => {
|
|
11457
|
+
d.run(`INSERT OR IGNORE INTO task_run_transactions (
|
|
11458
|
+
id, task_id, run_id, key, loop_id, loop_run_id, metadata, created_at, updated_at
|
|
11459
|
+
) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?)`, [
|
|
11460
|
+
uuid(),
|
|
11461
|
+
input.task_id,
|
|
11462
|
+
key,
|
|
11463
|
+
input.loop_id ?? null,
|
|
11464
|
+
input.loop_run_id ?? null,
|
|
11465
|
+
JSON.stringify(metadata),
|
|
11466
|
+
timestamp,
|
|
11467
|
+
timestamp
|
|
11468
|
+
]);
|
|
11469
|
+
const transaction = getTaskRunTransactionByKey(key, input.task_id, d);
|
|
11470
|
+
if (!transaction)
|
|
11471
|
+
throw new Error(`Could not create run transaction for key: ${key}`);
|
|
11472
|
+
if (transaction.run_id) {
|
|
11473
|
+
const existingRun = getTaskRun(transaction.run_id, d);
|
|
11474
|
+
if (existingRun)
|
|
11475
|
+
return { run: existingRun, action: "matched" };
|
|
11476
|
+
}
|
|
11477
|
+
const run = startTaskRun({
|
|
11478
|
+
id: uuid(),
|
|
11479
|
+
task_id: input.task_id,
|
|
11480
|
+
agent_id: input.agent_id,
|
|
11481
|
+
title: input.title,
|
|
11482
|
+
summary: input.summary,
|
|
11483
|
+
metadata,
|
|
11484
|
+
claim: input.claim,
|
|
11485
|
+
started_at: timestamp
|
|
11486
|
+
}, d);
|
|
11487
|
+
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]);
|
|
11488
|
+
return { run, action: "created" };
|
|
11489
|
+
})();
|
|
11490
|
+
return {
|
|
11491
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
11492
|
+
local_only: true,
|
|
11493
|
+
dry_run: false,
|
|
11494
|
+
processed_at: timestamp,
|
|
11495
|
+
action: created.action,
|
|
11496
|
+
key,
|
|
11497
|
+
run: summarizeTaskRun(created.run),
|
|
11498
|
+
warnings: [],
|
|
11499
|
+
commands: loopRunCommands(created.run, key)
|
|
11500
|
+
};
|
|
11501
|
+
}
|
|
11091
11502
|
function addTaskRunEvent(input, db) {
|
|
11092
11503
|
const d = db || getDatabase();
|
|
11093
11504
|
const runId = resolveTaskRunId(input.run_id, d);
|
|
@@ -11267,6 +11678,66 @@ function finishTaskRun(input, db) {
|
|
|
11267
11678
|
});
|
|
11268
11679
|
return updated;
|
|
11269
11680
|
}
|
|
11681
|
+
function finishTaskRunTransaction(input, db) {
|
|
11682
|
+
const d = db || getDatabase();
|
|
11683
|
+
const timestamp = input.completed_at || now();
|
|
11684
|
+
const status = input.status || "completed";
|
|
11685
|
+
const key = input.key ? normalizeTransactionKey({ key: input.key }) : "";
|
|
11686
|
+
const run = input.run_id ? getTaskRun(resolveTaskRunId(input.run_id, d), d) : key ? findTaskRunByTransactionKey(key, input.task_id, d) : null;
|
|
11687
|
+
if (!run) {
|
|
11688
|
+
throw new Error(input.run_id ? `Run not found: ${input.run_id}` : "runs finish requires a run id or --key");
|
|
11689
|
+
}
|
|
11690
|
+
if (input.task_id && run.task_id !== input.task_id) {
|
|
11691
|
+
throw new Error(`Run ${run.id} belongs to task ${run.task_id}, not ${input.task_id}`);
|
|
11692
|
+
}
|
|
11693
|
+
const resolvedKey = key || runKey(run) || run.id;
|
|
11694
|
+
const dryRun = input.apply === false;
|
|
11695
|
+
if (run.status !== "running") {
|
|
11696
|
+
const conflict = run.status !== status;
|
|
11697
|
+
return {
|
|
11698
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
11699
|
+
local_only: true,
|
|
11700
|
+
dry_run: dryRun,
|
|
11701
|
+
processed_at: timestamp,
|
|
11702
|
+
action: conflict ? "conflict" : "matched",
|
|
11703
|
+
key: resolvedKey,
|
|
11704
|
+
run: summarizeTaskRun(run),
|
|
11705
|
+
warnings: conflict ? [`run is already ${run.status}; requested ${status}`] : [],
|
|
11706
|
+
commands: loopRunCommands(run, resolvedKey)
|
|
11707
|
+
};
|
|
11708
|
+
}
|
|
11709
|
+
if (dryRun) {
|
|
11710
|
+
return {
|
|
11711
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
11712
|
+
local_only: true,
|
|
11713
|
+
dry_run: true,
|
|
11714
|
+
processed_at: timestamp,
|
|
11715
|
+
action: "preview",
|
|
11716
|
+
key: resolvedKey,
|
|
11717
|
+
run: summarizeTaskRun({ ...run, status, summary: input.summary ?? run.summary, completed_at: timestamp, updated_at: timestamp }),
|
|
11718
|
+
warnings: [],
|
|
11719
|
+
commands: loopRunCommands(run, resolvedKey)
|
|
11720
|
+
};
|
|
11721
|
+
}
|
|
11722
|
+
const finished = finishTaskRun({
|
|
11723
|
+
run_id: run.id,
|
|
11724
|
+
status,
|
|
11725
|
+
summary: input.summary,
|
|
11726
|
+
agent_id: input.agent_id,
|
|
11727
|
+
completed_at: timestamp
|
|
11728
|
+
}, d);
|
|
11729
|
+
return {
|
|
11730
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
11731
|
+
local_only: true,
|
|
11732
|
+
dry_run: false,
|
|
11733
|
+
processed_at: timestamp,
|
|
11734
|
+
action: "finished",
|
|
11735
|
+
key: resolvedKey,
|
|
11736
|
+
run: summarizeTaskRun(finished),
|
|
11737
|
+
warnings: [],
|
|
11738
|
+
commands: loopRunCommands(finished, resolvedKey)
|
|
11739
|
+
};
|
|
11740
|
+
}
|
|
11270
11741
|
function listTaskRuns(taskId, db) {
|
|
11271
11742
|
const d = db || getDatabase();
|
|
11272
11743
|
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();
|
|
@@ -17447,7 +17918,7 @@ function countMovedStandaloneVerifications(db, duplicateId) {
|
|
|
17447
17918
|
const rows = db.query("SELECT command FROM task_verifications WHERE task_id = ?").all(duplicateId);
|
|
17448
17919
|
return rows.filter((row) => !generatedCommands.has(row.command)).length;
|
|
17449
17920
|
}
|
|
17450
|
-
function
|
|
17921
|
+
function mergeTaskMetadata2(primary, duplicate, input, mergedAt) {
|
|
17451
17922
|
const mergedDuplicates = Array.isArray(primary.metadata["merged_duplicates"]) ? [...primary.metadata["merged_duplicates"]] : [];
|
|
17452
17923
|
mergedDuplicates.push({
|
|
17453
17924
|
id: duplicate.id,
|
|
@@ -17508,7 +17979,7 @@ function mergeDuplicateTask(input, db) {
|
|
|
17508
17979
|
updateTask(primary.id, {
|
|
17509
17980
|
version: primary.version,
|
|
17510
17981
|
tags: mergedTags,
|
|
17511
|
-
metadata:
|
|
17982
|
+
metadata: mergeTaskMetadata2(primary, duplicate, input, mergedAt),
|
|
17512
17983
|
description: mergeTaskDescription(primary, duplicate) ?? undefined
|
|
17513
17984
|
}, d);
|
|
17514
17985
|
moved.comments = updateRows(d, "task_comments", "task_id", duplicate.id, primary.id);
|
|
@@ -19979,6 +20450,33 @@ var TODOS_API_ROUTES = [
|
|
|
19979
20450
|
tags: ["tasks", "mutation"],
|
|
19980
20451
|
stability: "stable"
|
|
19981
20452
|
},
|
|
20453
|
+
{
|
|
20454
|
+
id: "tasks.upsert",
|
|
20455
|
+
method: "POST",
|
|
20456
|
+
path: "/api/tasks/upsert",
|
|
20457
|
+
description: "Create or update a task by stable metadata fingerprint, merging metadata on updates.",
|
|
20458
|
+
auth: "optional-api-key",
|
|
20459
|
+
requestSchema: {
|
|
20460
|
+
type: "object",
|
|
20461
|
+
properties: {
|
|
20462
|
+
fingerprint: { type: "string" },
|
|
20463
|
+
title: { type: "string" },
|
|
20464
|
+
description: { type: "string" },
|
|
20465
|
+
priority: { type: "string", enum: TASK_PRIORITIES },
|
|
20466
|
+
status: { type: "string", enum: TASK_STATUSES },
|
|
20467
|
+
project_id: { type: "string" },
|
|
20468
|
+
task_list_id: { type: "string" },
|
|
20469
|
+
working_dir: { type: "string" },
|
|
20470
|
+
tags: { type: "array", items: { type: "string" } },
|
|
20471
|
+
metadata: objectSchema
|
|
20472
|
+
},
|
|
20473
|
+
required: ["fingerprint", "title"],
|
|
20474
|
+
additionalProperties: true
|
|
20475
|
+
},
|
|
20476
|
+
responseSchema: objectSchema,
|
|
20477
|
+
tags: ["tasks", "mutation", "dedupe"],
|
|
20478
|
+
stability: "stable"
|
|
20479
|
+
},
|
|
19982
20480
|
{
|
|
19983
20481
|
id: "tasks.read",
|
|
19984
20482
|
method: "GET",
|