@hasna/todos 0.11.58 → 0.11.60
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 +66 -2
- 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 +1518 -197
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +600 -14
- 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 +968 -15
- 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 +1082 -27
- 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 +600 -14
- package/dist/release-provenance.json +3 -3
- package/dist/server/index.js +1082 -27
- 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 +473 -11
- package/dist/types/index.d.ts +11 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +2 -2
package/dist/registry.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",
|
|
@@ -6773,7 +6955,7 @@ async function testLocalEventHook(name, input) {
|
|
|
6773
6955
|
return emitLocalEventHooks({ ...input, hooks: [hook] });
|
|
6774
6956
|
}
|
|
6775
6957
|
|
|
6776
|
-
// node_modules/.bun/@hasna+events@0.1.
|
|
6958
|
+
// node_modules/.bun/@hasna+events@0.1.10/node_modules/@hasna/events/dist/index.js
|
|
6777
6959
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
6778
6960
|
import { existsSync as existsSync6 } from "fs";
|
|
6779
6961
|
import { homedir } from "os";
|
|
@@ -6820,14 +7002,40 @@ function matchRecord(input, matcher) {
|
|
|
6820
7002
|
return true;
|
|
6821
7003
|
return Object.entries(matcher).every(([path, expected]) => {
|
|
6822
7004
|
const actual = getPathValue(input, path);
|
|
6823
|
-
|
|
6824
|
-
return matchString(actual === undefined ? undefined : String(actual), expected, {
|
|
6825
|
-
segmentSafe: path.endsWith("_path") || path.endsWith(".path")
|
|
6826
|
-
});
|
|
6827
|
-
}
|
|
6828
|
-
return actual === expected;
|
|
7005
|
+
return matchField(actual, expected, path);
|
|
6829
7006
|
});
|
|
6830
7007
|
}
|
|
7008
|
+
function matchField(actual, expected, path) {
|
|
7009
|
+
if (isNegativeMatcher(expected)) {
|
|
7010
|
+
return !matchPositiveField(actual, expected.not, path);
|
|
7011
|
+
}
|
|
7012
|
+
return matchPositiveField(actual, expected, path);
|
|
7013
|
+
}
|
|
7014
|
+
function matchPositiveField(actual, expected, path) {
|
|
7015
|
+
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
7016
|
+
return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
|
|
7017
|
+
segmentSafe: path.endsWith("_path") || path.endsWith(".path")
|
|
7018
|
+
}));
|
|
7019
|
+
}
|
|
7020
|
+
if (Array.isArray(actual)) {
|
|
7021
|
+
return actual.some((item) => item === expected);
|
|
7022
|
+
}
|
|
7023
|
+
return actual === expected;
|
|
7024
|
+
}
|
|
7025
|
+
function stringCandidates(actual) {
|
|
7026
|
+
if (actual === undefined)
|
|
7027
|
+
return [];
|
|
7028
|
+
if (Array.isArray(actual)) {
|
|
7029
|
+
return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
|
|
7030
|
+
}
|
|
7031
|
+
return [String(actual)];
|
|
7032
|
+
}
|
|
7033
|
+
function isPrimitiveFieldValue(value) {
|
|
7034
|
+
return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
7035
|
+
}
|
|
7036
|
+
function isNegativeMatcher(value) {
|
|
7037
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
|
|
7038
|
+
}
|
|
6831
7039
|
function eventMatchesFilter(event, filter) {
|
|
6832
7040
|
return matchString(event.source, filter.source) && matchString(event.type, filter.type) && matchString(event.subject, filter.subject) && matchString(event.severity, filter.severity) && matchRecord(event.data, filter.data) && matchRecord(event.metadata, filter.metadata);
|
|
6833
7041
|
}
|
|
@@ -7434,9 +7642,66 @@ function taskEventData(task, extra = {}) {
|
|
|
7434
7642
|
started_at: task.started_at,
|
|
7435
7643
|
completed_at: task.completed_at,
|
|
7436
7644
|
due_at: task.due_at,
|
|
7645
|
+
requires_approval: task.requires_approval,
|
|
7646
|
+
approved_by: task.approved_by,
|
|
7647
|
+
approved_at: task.approved_at,
|
|
7437
7648
|
...extra
|
|
7438
7649
|
};
|
|
7439
7650
|
}
|
|
7651
|
+
function booleanField(value) {
|
|
7652
|
+
if (typeof value === "boolean")
|
|
7653
|
+
return value;
|
|
7654
|
+
if (typeof value === "number") {
|
|
7655
|
+
if (value === 1)
|
|
7656
|
+
return true;
|
|
7657
|
+
if (value === 0)
|
|
7658
|
+
return false;
|
|
7659
|
+
}
|
|
7660
|
+
if (typeof value === "string") {
|
|
7661
|
+
const normalized = value.trim().toLowerCase();
|
|
7662
|
+
if (["true", "1", "yes", "on"].includes(normalized))
|
|
7663
|
+
return true;
|
|
7664
|
+
if (["false", "0", "no", "off"].includes(normalized))
|
|
7665
|
+
return false;
|
|
7666
|
+
}
|
|
7667
|
+
return;
|
|
7668
|
+
}
|
|
7669
|
+
function objectField(value) {
|
|
7670
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
7671
|
+
}
|
|
7672
|
+
function firstBoolean(records, keys) {
|
|
7673
|
+
for (const record of records) {
|
|
7674
|
+
for (const key of keys) {
|
|
7675
|
+
const value = booleanField(record[key]);
|
|
7676
|
+
if (value !== undefined)
|
|
7677
|
+
return value;
|
|
7678
|
+
}
|
|
7679
|
+
}
|
|
7680
|
+
return;
|
|
7681
|
+
}
|
|
7682
|
+
function routingAutomationMetadata(task) {
|
|
7683
|
+
const automation = objectField(task.metadata.automation);
|
|
7684
|
+
const records = [task.metadata];
|
|
7685
|
+
if (automation)
|
|
7686
|
+
records.push(automation);
|
|
7687
|
+
const result = {};
|
|
7688
|
+
const aliases = [
|
|
7689
|
+
["allowed", ["allowed", "automation_allowed", "automationAllowed"]],
|
|
7690
|
+
["no_auto", ["no_auto", "noAuto"]],
|
|
7691
|
+
["manual", ["manual"]],
|
|
7692
|
+
["manual_required", ["manual_required", "manualRequired"]],
|
|
7693
|
+
["requires_approval", ["requires_approval", "requiresApproval"]],
|
|
7694
|
+
["approval_required", ["approval_required", "approvalRequired"]]
|
|
7695
|
+
];
|
|
7696
|
+
for (const [canonical, keys] of aliases) {
|
|
7697
|
+
const value = firstBoolean(records, keys);
|
|
7698
|
+
if (value !== undefined)
|
|
7699
|
+
result[canonical] = value;
|
|
7700
|
+
}
|
|
7701
|
+
if (task.requires_approval)
|
|
7702
|
+
result.requires_approval = true;
|
|
7703
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
7704
|
+
}
|
|
7440
7705
|
function taskEventMetadata(task) {
|
|
7441
7706
|
const metadata = {
|
|
7442
7707
|
package: "@hasna/todos",
|
|
@@ -7447,6 +7712,14 @@ function taskEventMetadata(task) {
|
|
|
7447
7712
|
task_list_id: task.task_list_id,
|
|
7448
7713
|
working_dir: task.working_dir
|
|
7449
7714
|
};
|
|
7715
|
+
const routeEnabled = booleanField(task.metadata.route_enabled);
|
|
7716
|
+
if (routeEnabled !== undefined) {
|
|
7717
|
+
metadata.route_enabled = routeEnabled;
|
|
7718
|
+
}
|
|
7719
|
+
const automation = routingAutomationMetadata(task);
|
|
7720
|
+
if (automation) {
|
|
7721
|
+
metadata.automation = automation;
|
|
7722
|
+
}
|
|
7450
7723
|
try {
|
|
7451
7724
|
const project = task.project_id ? getProject(task.project_id) : null;
|
|
7452
7725
|
const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
|
|
@@ -7464,9 +7737,6 @@ function taskEventMetadata(task) {
|
|
|
7464
7737
|
if (projectPath) {
|
|
7465
7738
|
metadata.project_kind = classifyProjectKind(projectPath);
|
|
7466
7739
|
metadata.project_is_worktree = isWorktreePath(projectPath);
|
|
7467
|
-
if (typeof task.metadata.route_enabled === "boolean") {
|
|
7468
|
-
metadata.route_enabled = task.metadata.route_enabled;
|
|
7469
|
-
}
|
|
7470
7740
|
metadata.working_dir = task.working_dir ?? projectPath;
|
|
7471
7741
|
}
|
|
7472
7742
|
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;
|
|
@@ -7873,6 +8143,17 @@ function replaceTaskTags(taskId, tags, db) {
|
|
|
7873
8143
|
db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
|
|
7874
8144
|
insertTaskTags(taskId, tags, db);
|
|
7875
8145
|
}
|
|
8146
|
+
function addMetadataConditions(metadata, conditions, params) {
|
|
8147
|
+
if (!metadata)
|
|
8148
|
+
return;
|
|
8149
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
8150
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(key)) {
|
|
8151
|
+
throw new Error(`Invalid metadata filter key: ${key}`);
|
|
8152
|
+
}
|
|
8153
|
+
conditions.push(`json_extract(metadata, '$."${key}"') = ?`);
|
|
8154
|
+
params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
|
|
8155
|
+
}
|
|
8156
|
+
}
|
|
7876
8157
|
function createTask(input, db) {
|
|
7877
8158
|
const d = db || getDatabase();
|
|
7878
8159
|
const timestamp = now();
|
|
@@ -8055,6 +8336,7 @@ function listTasks(filter = {}, db) {
|
|
|
8055
8336
|
params.push(filter.task_type);
|
|
8056
8337
|
}
|
|
8057
8338
|
}
|
|
8339
|
+
addMetadataConditions(filter.metadata, conditions, params);
|
|
8058
8340
|
const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
|
|
8059
8341
|
if (filter.cursor) {
|
|
8060
8342
|
try {
|
|
@@ -8079,6 +8361,54 @@ function listTasks(filter = {}, db) {
|
|
|
8079
8361
|
const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC${limitClause}`).all(...params);
|
|
8080
8362
|
return rows.map(rowToTask);
|
|
8081
8363
|
}
|
|
8364
|
+
function getTaskByFingerprint(fingerprint, db) {
|
|
8365
|
+
const tasks = listTasks({ metadata: { fingerprint }, limit: 1 }, db);
|
|
8366
|
+
return tasks[0] ?? null;
|
|
8367
|
+
}
|
|
8368
|
+
function mergeTaskMetadata(current, next, fingerprint) {
|
|
8369
|
+
return {
|
|
8370
|
+
...current,
|
|
8371
|
+
...next ?? {},
|
|
8372
|
+
fingerprint
|
|
8373
|
+
};
|
|
8374
|
+
}
|
|
8375
|
+
function upsertTaskByFingerprint(input, db) {
|
|
8376
|
+
const d = db || getDatabase();
|
|
8377
|
+
const fingerprint = input.fingerprint.trim();
|
|
8378
|
+
if (!fingerprint)
|
|
8379
|
+
throw new Error("fingerprint is required");
|
|
8380
|
+
const existing = getTaskByFingerprint(fingerprint, d);
|
|
8381
|
+
const metadata = mergeTaskMetadata(existing?.metadata ?? {}, input.metadata, fingerprint);
|
|
8382
|
+
if (!existing) {
|
|
8383
|
+
const task2 = createTask({ ...input, metadata }, d);
|
|
8384
|
+
return { task: task2, created: true };
|
|
8385
|
+
}
|
|
8386
|
+
const task = updateTask(existing.id, {
|
|
8387
|
+
version: existing.version,
|
|
8388
|
+
title: input.title,
|
|
8389
|
+
description: input.description,
|
|
8390
|
+
status: input.status,
|
|
8391
|
+
priority: input.priority,
|
|
8392
|
+
project_id: input.project_id,
|
|
8393
|
+
assigned_to: input.assigned_to,
|
|
8394
|
+
working_dir: input.working_dir,
|
|
8395
|
+
plan_id: input.plan_id,
|
|
8396
|
+
task_list_id: input.task_list_id,
|
|
8397
|
+
tags: input.tags,
|
|
8398
|
+
metadata,
|
|
8399
|
+
due_at: input.due_at,
|
|
8400
|
+
estimated_minutes: input.estimated_minutes,
|
|
8401
|
+
sla_minutes: input.sla_minutes,
|
|
8402
|
+
confidence: input.confidence,
|
|
8403
|
+
retry_count: input.retry_count,
|
|
8404
|
+
max_retries: input.max_retries,
|
|
8405
|
+
retry_after: input.retry_after,
|
|
8406
|
+
requires_approval: input.requires_approval,
|
|
8407
|
+
recurrence_rule: input.recurrence_rule,
|
|
8408
|
+
task_type: input.task_type
|
|
8409
|
+
}, d);
|
|
8410
|
+
return { task, created: false };
|
|
8411
|
+
}
|
|
8082
8412
|
function countTasks(filter = {}, db) {
|
|
8083
8413
|
const d = db || getDatabase();
|
|
8084
8414
|
const conditions = [];
|
|
@@ -8142,6 +8472,7 @@ function countTasks(filter = {}, db) {
|
|
|
8142
8472
|
conditions.push("task_list_id = ?");
|
|
8143
8473
|
params.push(filter.task_list_id);
|
|
8144
8474
|
}
|
|
8475
|
+
addMetadataConditions(filter.metadata, conditions, params);
|
|
8145
8476
|
if (!filter.include_archived) {
|
|
8146
8477
|
conditions.push("archived_at IS NULL");
|
|
8147
8478
|
}
|
|
@@ -8192,6 +8523,10 @@ function updateTask(id, input, db) {
|
|
|
8192
8523
|
sets.push("assigned_to = ?");
|
|
8193
8524
|
params.push(input.assigned_to);
|
|
8194
8525
|
}
|
|
8526
|
+
if (input.working_dir !== undefined) {
|
|
8527
|
+
sets.push("working_dir = ?");
|
|
8528
|
+
params.push(input.working_dir);
|
|
8529
|
+
}
|
|
8195
8530
|
if (input.tags !== undefined) {
|
|
8196
8531
|
sets.push("tags = ?");
|
|
8197
8532
|
params.push(JSON.stringify(input.tags));
|
|
@@ -8280,6 +8615,8 @@ function updateTask(id, input, db) {
|
|
|
8280
8615
|
logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
|
|
8281
8616
|
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
|
|
8282
8617
|
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
8618
|
+
if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
|
|
8619
|
+
logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
|
|
8283
8620
|
if (input.approved_by !== undefined)
|
|
8284
8621
|
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
8285
8622
|
const updatedTask = {
|
|
@@ -8315,6 +8652,10 @@ function updateTask(id, input, db) {
|
|
|
8315
8652
|
if (input.approved_by !== undefined) {
|
|
8316
8653
|
emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
|
|
8317
8654
|
}
|
|
8655
|
+
const updatePayload = taskEventData(updatedTask);
|
|
8656
|
+
dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
|
|
8657
|
+
emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload });
|
|
8658
|
+
emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask });
|
|
8318
8659
|
return updatedTask;
|
|
8319
8660
|
}
|
|
8320
8661
|
function deleteTask(id, db) {
|
|
@@ -11010,6 +11351,7 @@ function getTaskTraceability(taskId, db) {
|
|
|
11010
11351
|
|
|
11011
11352
|
// src/db/task-runs.ts
|
|
11012
11353
|
init_redaction();
|
|
11354
|
+
var LOOP_RUN_TRANSACTION_SCHEMA_VERSION = "todos.loop_run_transaction.v1";
|
|
11013
11355
|
function parseObject(value) {
|
|
11014
11356
|
if (!value)
|
|
11015
11357
|
return {};
|
|
@@ -11032,6 +11374,72 @@ function rowToArtifact(row) {
|
|
|
11032
11374
|
function getRunRow(runId, db) {
|
|
11033
11375
|
return db.query("SELECT * FROM task_runs WHERE id = ?").get(runId);
|
|
11034
11376
|
}
|
|
11377
|
+
function normalizeTransactionKey(input) {
|
|
11378
|
+
const key = (input.key || input.loop_run_id || input.loop_id || "").trim();
|
|
11379
|
+
if (!key)
|
|
11380
|
+
throw new Error("idempotent run transactions require --key, --loop-run-id, or --loop-id");
|
|
11381
|
+
return key.toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 240);
|
|
11382
|
+
}
|
|
11383
|
+
function loopTransactionMetadata(record) {
|
|
11384
|
+
const value = record.metadata["loop_transaction"];
|
|
11385
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
11386
|
+
}
|
|
11387
|
+
function runKey(record) {
|
|
11388
|
+
const tx = loopTransactionMetadata(record);
|
|
11389
|
+
const key = tx["idempotency_key"] ?? record.metadata["idempotency_key"];
|
|
11390
|
+
return typeof key === "string" ? key : null;
|
|
11391
|
+
}
|
|
11392
|
+
function loopId(record) {
|
|
11393
|
+
const tx = loopTransactionMetadata(record);
|
|
11394
|
+
const value = tx["loop_id"] ?? record.metadata["loop_id"];
|
|
11395
|
+
return typeof value === "string" ? value : null;
|
|
11396
|
+
}
|
|
11397
|
+
function loopRunId(record) {
|
|
11398
|
+
const tx = loopTransactionMetadata(record);
|
|
11399
|
+
const value = tx["loop_run_id"] ?? record.metadata["loop_run_id"];
|
|
11400
|
+
return typeof value === "string" ? value : null;
|
|
11401
|
+
}
|
|
11402
|
+
function getTaskRunTransactionByKey(key, taskId, db) {
|
|
11403
|
+
if (taskId) {
|
|
11404
|
+
return db.query("SELECT * FROM task_run_transactions WHERE task_id = ? AND key = ?").get(taskId, key);
|
|
11405
|
+
}
|
|
11406
|
+
const rows = db.query("SELECT * FROM task_run_transactions WHERE key = ? ORDER BY created_at DESC LIMIT 2").all(key);
|
|
11407
|
+
if (rows.length > 1)
|
|
11408
|
+
throw new Error(`Run transaction key is ambiguous across tasks: ${key}. Pass task_id.`);
|
|
11409
|
+
return rows[0] ?? null;
|
|
11410
|
+
}
|
|
11411
|
+
function summarizeTaskRun(run) {
|
|
11412
|
+
return {
|
|
11413
|
+
id: run.id,
|
|
11414
|
+
task_id: run.task_id,
|
|
11415
|
+
agent_id: run.agent_id,
|
|
11416
|
+
title: run.title,
|
|
11417
|
+
status: run.status,
|
|
11418
|
+
summary: run.summary,
|
|
11419
|
+
idempotency_key: runKey(run),
|
|
11420
|
+
loop_id: loopId(run),
|
|
11421
|
+
loop_run_id: loopRunId(run),
|
|
11422
|
+
metadata_keys: Object.keys(run.metadata).sort(),
|
|
11423
|
+
started_at: run.started_at,
|
|
11424
|
+
completed_at: run.completed_at,
|
|
11425
|
+
updated_at: run.updated_at
|
|
11426
|
+
};
|
|
11427
|
+
}
|
|
11428
|
+
function findTaskRunByTransactionKey(key, taskId, db) {
|
|
11429
|
+
const d = db || getDatabase();
|
|
11430
|
+
const normalized = normalizeTransactionKey({ key });
|
|
11431
|
+
const transaction = getTaskRunTransactionByKey(normalized, taskId, d);
|
|
11432
|
+
if (transaction?.run_id)
|
|
11433
|
+
return getTaskRun(transaction.run_id, d);
|
|
11434
|
+
return null;
|
|
11435
|
+
}
|
|
11436
|
+
function loopRunCommands(run, key) {
|
|
11437
|
+
return [
|
|
11438
|
+
run ? `todos runs show ${run.id.slice(0, 8)}` : "todos runs list",
|
|
11439
|
+
run ? `todos findings list --task ${run.task_id.slice(0, 8)} --json` : "todos findings list --json",
|
|
11440
|
+
`todos runs begin <task-id> --key ${key} --apply --json`
|
|
11441
|
+
];
|
|
11442
|
+
}
|
|
11035
11443
|
function resolveTaskRunId(idOrPrefix, db) {
|
|
11036
11444
|
const d = db || getDatabase();
|
|
11037
11445
|
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 +11458,7 @@ function startTaskRun(input, db) {
|
|
|
11050
11458
|
const d = db || getDatabase();
|
|
11051
11459
|
if (!getTask(input.task_id, d))
|
|
11052
11460
|
throw new TaskNotFoundError(input.task_id);
|
|
11053
|
-
const id = uuid();
|
|
11461
|
+
const id = input.id ?? uuid();
|
|
11054
11462
|
const timestamp = input.started_at || now();
|
|
11055
11463
|
if (input.claim && input.agent_id) {
|
|
11056
11464
|
startTask(input.task_id, input.agent_id, d);
|
|
@@ -11088,6 +11496,97 @@ function startTaskRun(input, db) {
|
|
|
11088
11496
|
emitLocalEventHooksQuiet({ type: "run.started", payload: { id: run.id, task_id: run.task_id, agent_id: run.agent_id, title: run.title } });
|
|
11089
11497
|
return run;
|
|
11090
11498
|
}
|
|
11499
|
+
function beginTaskRunTransaction(input, db) {
|
|
11500
|
+
const d = db || getDatabase();
|
|
11501
|
+
if (!getTask(input.task_id, d))
|
|
11502
|
+
throw new TaskNotFoundError(input.task_id);
|
|
11503
|
+
const timestamp = input.started_at || now();
|
|
11504
|
+
const key = normalizeTransactionKey(input);
|
|
11505
|
+
const existing = findTaskRunByTransactionKey(key, input.task_id, d);
|
|
11506
|
+
const dryRun = !input.apply;
|
|
11507
|
+
if (existing) {
|
|
11508
|
+
return {
|
|
11509
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
11510
|
+
local_only: true,
|
|
11511
|
+
dry_run: dryRun,
|
|
11512
|
+
processed_at: timestamp,
|
|
11513
|
+
action: "matched",
|
|
11514
|
+
key,
|
|
11515
|
+
run: summarizeTaskRun(existing),
|
|
11516
|
+
warnings: existing.status === "running" ? [] : [`matched ${existing.status} run`],
|
|
11517
|
+
commands: loopRunCommands(existing, key)
|
|
11518
|
+
};
|
|
11519
|
+
}
|
|
11520
|
+
if (dryRun) {
|
|
11521
|
+
return {
|
|
11522
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
11523
|
+
local_only: true,
|
|
11524
|
+
dry_run: true,
|
|
11525
|
+
processed_at: timestamp,
|
|
11526
|
+
action: "preview",
|
|
11527
|
+
key,
|
|
11528
|
+
run: null,
|
|
11529
|
+
warnings: [],
|
|
11530
|
+
commands: loopRunCommands(null, key)
|
|
11531
|
+
};
|
|
11532
|
+
}
|
|
11533
|
+
const metadata = redactValue({
|
|
11534
|
+
...input.metadata || {},
|
|
11535
|
+
loop_transaction: {
|
|
11536
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
11537
|
+
idempotency_key: key,
|
|
11538
|
+
loop_id: input.loop_id ?? null,
|
|
11539
|
+
loop_run_id: input.loop_run_id ?? null,
|
|
11540
|
+
first_seen_at: timestamp
|
|
11541
|
+
},
|
|
11542
|
+
idempotency_key: key
|
|
11543
|
+
});
|
|
11544
|
+
const created = d.transaction(() => {
|
|
11545
|
+
d.run(`INSERT OR IGNORE INTO task_run_transactions (
|
|
11546
|
+
id, task_id, run_id, key, loop_id, loop_run_id, metadata, created_at, updated_at
|
|
11547
|
+
) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?)`, [
|
|
11548
|
+
uuid(),
|
|
11549
|
+
input.task_id,
|
|
11550
|
+
key,
|
|
11551
|
+
input.loop_id ?? null,
|
|
11552
|
+
input.loop_run_id ?? null,
|
|
11553
|
+
JSON.stringify(metadata),
|
|
11554
|
+
timestamp,
|
|
11555
|
+
timestamp
|
|
11556
|
+
]);
|
|
11557
|
+
const transaction = getTaskRunTransactionByKey(key, input.task_id, d);
|
|
11558
|
+
if (!transaction)
|
|
11559
|
+
throw new Error(`Could not create run transaction for key: ${key}`);
|
|
11560
|
+
if (transaction.run_id) {
|
|
11561
|
+
const existingRun = getTaskRun(transaction.run_id, d);
|
|
11562
|
+
if (existingRun)
|
|
11563
|
+
return { run: existingRun, action: "matched" };
|
|
11564
|
+
}
|
|
11565
|
+
const run = startTaskRun({
|
|
11566
|
+
id: uuid(),
|
|
11567
|
+
task_id: input.task_id,
|
|
11568
|
+
agent_id: input.agent_id,
|
|
11569
|
+
title: input.title,
|
|
11570
|
+
summary: input.summary,
|
|
11571
|
+
metadata,
|
|
11572
|
+
claim: input.claim,
|
|
11573
|
+
started_at: timestamp
|
|
11574
|
+
}, d);
|
|
11575
|
+
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]);
|
|
11576
|
+
return { run, action: "created" };
|
|
11577
|
+
})();
|
|
11578
|
+
return {
|
|
11579
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
11580
|
+
local_only: true,
|
|
11581
|
+
dry_run: false,
|
|
11582
|
+
processed_at: timestamp,
|
|
11583
|
+
action: created.action,
|
|
11584
|
+
key,
|
|
11585
|
+
run: summarizeTaskRun(created.run),
|
|
11586
|
+
warnings: [],
|
|
11587
|
+
commands: loopRunCommands(created.run, key)
|
|
11588
|
+
};
|
|
11589
|
+
}
|
|
11091
11590
|
function addTaskRunEvent(input, db) {
|
|
11092
11591
|
const d = db || getDatabase();
|
|
11093
11592
|
const runId = resolveTaskRunId(input.run_id, d);
|
|
@@ -11267,6 +11766,66 @@ function finishTaskRun(input, db) {
|
|
|
11267
11766
|
});
|
|
11268
11767
|
return updated;
|
|
11269
11768
|
}
|
|
11769
|
+
function finishTaskRunTransaction(input, db) {
|
|
11770
|
+
const d = db || getDatabase();
|
|
11771
|
+
const timestamp = input.completed_at || now();
|
|
11772
|
+
const status = input.status || "completed";
|
|
11773
|
+
const key = input.key ? normalizeTransactionKey({ key: input.key }) : "";
|
|
11774
|
+
const run = input.run_id ? getTaskRun(resolveTaskRunId(input.run_id, d), d) : key ? findTaskRunByTransactionKey(key, input.task_id, d) : null;
|
|
11775
|
+
if (!run) {
|
|
11776
|
+
throw new Error(input.run_id ? `Run not found: ${input.run_id}` : "runs finish requires a run id or --key");
|
|
11777
|
+
}
|
|
11778
|
+
if (input.task_id && run.task_id !== input.task_id) {
|
|
11779
|
+
throw new Error(`Run ${run.id} belongs to task ${run.task_id}, not ${input.task_id}`);
|
|
11780
|
+
}
|
|
11781
|
+
const resolvedKey = key || runKey(run) || run.id;
|
|
11782
|
+
const dryRun = input.apply === false;
|
|
11783
|
+
if (run.status !== "running") {
|
|
11784
|
+
const conflict = run.status !== status;
|
|
11785
|
+
return {
|
|
11786
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
11787
|
+
local_only: true,
|
|
11788
|
+
dry_run: dryRun,
|
|
11789
|
+
processed_at: timestamp,
|
|
11790
|
+
action: conflict ? "conflict" : "matched",
|
|
11791
|
+
key: resolvedKey,
|
|
11792
|
+
run: summarizeTaskRun(run),
|
|
11793
|
+
warnings: conflict ? [`run is already ${run.status}; requested ${status}`] : [],
|
|
11794
|
+
commands: loopRunCommands(run, resolvedKey)
|
|
11795
|
+
};
|
|
11796
|
+
}
|
|
11797
|
+
if (dryRun) {
|
|
11798
|
+
return {
|
|
11799
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
11800
|
+
local_only: true,
|
|
11801
|
+
dry_run: true,
|
|
11802
|
+
processed_at: timestamp,
|
|
11803
|
+
action: "preview",
|
|
11804
|
+
key: resolvedKey,
|
|
11805
|
+
run: summarizeTaskRun({ ...run, status, summary: input.summary ?? run.summary, completed_at: timestamp, updated_at: timestamp }),
|
|
11806
|
+
warnings: [],
|
|
11807
|
+
commands: loopRunCommands(run, resolvedKey)
|
|
11808
|
+
};
|
|
11809
|
+
}
|
|
11810
|
+
const finished = finishTaskRun({
|
|
11811
|
+
run_id: run.id,
|
|
11812
|
+
status,
|
|
11813
|
+
summary: input.summary,
|
|
11814
|
+
agent_id: input.agent_id,
|
|
11815
|
+
completed_at: timestamp
|
|
11816
|
+
}, d);
|
|
11817
|
+
return {
|
|
11818
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
11819
|
+
local_only: true,
|
|
11820
|
+
dry_run: false,
|
|
11821
|
+
processed_at: timestamp,
|
|
11822
|
+
action: "finished",
|
|
11823
|
+
key: resolvedKey,
|
|
11824
|
+
run: summarizeTaskRun(finished),
|
|
11825
|
+
warnings: [],
|
|
11826
|
+
commands: loopRunCommands(finished, resolvedKey)
|
|
11827
|
+
};
|
|
11828
|
+
}
|
|
11270
11829
|
function listTaskRuns(taskId, db) {
|
|
11271
11830
|
const d = db || getDatabase();
|
|
11272
11831
|
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 +18006,7 @@ function countMovedStandaloneVerifications(db, duplicateId) {
|
|
|
17447
18006
|
const rows = db.query("SELECT command FROM task_verifications WHERE task_id = ?").all(duplicateId);
|
|
17448
18007
|
return rows.filter((row) => !generatedCommands.has(row.command)).length;
|
|
17449
18008
|
}
|
|
17450
|
-
function
|
|
18009
|
+
function mergeTaskMetadata2(primary, duplicate, input, mergedAt) {
|
|
17451
18010
|
const mergedDuplicates = Array.isArray(primary.metadata["merged_duplicates"]) ? [...primary.metadata["merged_duplicates"]] : [];
|
|
17452
18011
|
mergedDuplicates.push({
|
|
17453
18012
|
id: duplicate.id,
|
|
@@ -17508,7 +18067,7 @@ function mergeDuplicateTask(input, db) {
|
|
|
17508
18067
|
updateTask(primary.id, {
|
|
17509
18068
|
version: primary.version,
|
|
17510
18069
|
tags: mergedTags,
|
|
17511
|
-
metadata:
|
|
18070
|
+
metadata: mergeTaskMetadata2(primary, duplicate, input, mergedAt),
|
|
17512
18071
|
description: mergeTaskDescription(primary, duplicate) ?? undefined
|
|
17513
18072
|
}, d);
|
|
17514
18073
|
moved.comments = updateRows(d, "task_comments", "task_id", duplicate.id, primary.id);
|
|
@@ -19979,6 +20538,33 @@ var TODOS_API_ROUTES = [
|
|
|
19979
20538
|
tags: ["tasks", "mutation"],
|
|
19980
20539
|
stability: "stable"
|
|
19981
20540
|
},
|
|
20541
|
+
{
|
|
20542
|
+
id: "tasks.upsert",
|
|
20543
|
+
method: "POST",
|
|
20544
|
+
path: "/api/tasks/upsert",
|
|
20545
|
+
description: "Create or update a task by stable metadata fingerprint, merging metadata on updates.",
|
|
20546
|
+
auth: "optional-api-key",
|
|
20547
|
+
requestSchema: {
|
|
20548
|
+
type: "object",
|
|
20549
|
+
properties: {
|
|
20550
|
+
fingerprint: { type: "string" },
|
|
20551
|
+
title: { type: "string" },
|
|
20552
|
+
description: { type: "string" },
|
|
20553
|
+
priority: { type: "string", enum: TASK_PRIORITIES },
|
|
20554
|
+
status: { type: "string", enum: TASK_STATUSES },
|
|
20555
|
+
project_id: { type: "string" },
|
|
20556
|
+
task_list_id: { type: "string" },
|
|
20557
|
+
working_dir: { type: "string" },
|
|
20558
|
+
tags: { type: "array", items: { type: "string" } },
|
|
20559
|
+
metadata: objectSchema
|
|
20560
|
+
},
|
|
20561
|
+
required: ["fingerprint", "title"],
|
|
20562
|
+
additionalProperties: true
|
|
20563
|
+
},
|
|
20564
|
+
responseSchema: objectSchema,
|
|
20565
|
+
tags: ["tasks", "mutation", "dedupe"],
|
|
20566
|
+
stability: "stable"
|
|
20567
|
+
},
|
|
19982
20568
|
{
|
|
19983
20569
|
id: "tasks.read",
|
|
19984
20570
|
method: "GET",
|