@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/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.9/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";
|
|
@@ -6790,17 +6972,30 @@ function getPathValue(input, path) {
|
|
|
6790
6972
|
return;
|
|
6791
6973
|
}, input);
|
|
6792
6974
|
}
|
|
6793
|
-
function wildcardToRegExp(pattern) {
|
|
6794
|
-
|
|
6795
|
-
|
|
6975
|
+
function wildcardToRegExp(pattern, options = {}) {
|
|
6976
|
+
let body = "";
|
|
6977
|
+
for (let index = 0;index < pattern.length; index += 1) {
|
|
6978
|
+
const char = pattern[index];
|
|
6979
|
+
if (char === "*") {
|
|
6980
|
+
if (pattern[index + 1] === "*") {
|
|
6981
|
+
body += ".*";
|
|
6982
|
+
index += 1;
|
|
6983
|
+
} else {
|
|
6984
|
+
body += options.segmentSafe ? "[^/]*" : ".*";
|
|
6985
|
+
}
|
|
6986
|
+
} else {
|
|
6987
|
+
body += char.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
6988
|
+
}
|
|
6989
|
+
}
|
|
6990
|
+
return new RegExp(`^${body}$`);
|
|
6796
6991
|
}
|
|
6797
|
-
function matchString(value, matcher) {
|
|
6992
|
+
function matchString(value, matcher, options = {}) {
|
|
6798
6993
|
if (matcher === undefined)
|
|
6799
6994
|
return true;
|
|
6800
6995
|
if (value === undefined)
|
|
6801
6996
|
return false;
|
|
6802
6997
|
const matchers = Array.isArray(matcher) ? matcher : [matcher];
|
|
6803
|
-
return matchers.some((item) => wildcardToRegExp(item).test(value));
|
|
6998
|
+
return matchers.some((item) => wildcardToRegExp(item, options).test(value));
|
|
6804
6999
|
}
|
|
6805
7000
|
function matchRecord(input, matcher) {
|
|
6806
7001
|
if (!matcher)
|
|
@@ -6808,7 +7003,9 @@ function matchRecord(input, matcher) {
|
|
|
6808
7003
|
return Object.entries(matcher).every(([path, expected]) => {
|
|
6809
7004
|
const actual = getPathValue(input, path);
|
|
6810
7005
|
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
6811
|
-
return matchString(actual === undefined ? undefined : String(actual), expected
|
|
7006
|
+
return matchString(actual === undefined ? undefined : String(actual), expected, {
|
|
7007
|
+
segmentSafe: path.endsWith("_path") || path.endsWith(".path")
|
|
7008
|
+
});
|
|
6812
7009
|
}
|
|
6813
7010
|
return actual === expected;
|
|
6814
7011
|
});
|
|
@@ -6828,7 +7025,6 @@ var HASNA_EVENTS_HOME_ENV = "HASNA_EVENTS_HOME";
|
|
|
6828
7025
|
function getEventsDataDir(override) {
|
|
6829
7026
|
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join5(homedir(), ".hasna", "events");
|
|
6830
7027
|
}
|
|
6831
|
-
|
|
6832
7028
|
class JsonEventsStore {
|
|
6833
7029
|
dataDir;
|
|
6834
7030
|
channelsPath;
|
|
@@ -7165,7 +7361,7 @@ class EventsClient {
|
|
|
7165
7361
|
}
|
|
7166
7362
|
return deliveries;
|
|
7167
7363
|
}
|
|
7168
|
-
async
|
|
7364
|
+
async matchChannel(id, input = {}) {
|
|
7169
7365
|
const channel = await this.store.getChannel(id);
|
|
7170
7366
|
if (!channel)
|
|
7171
7367
|
throw new Error(`Channel not found: ${id}`);
|
|
@@ -7182,6 +7378,34 @@ class EventsClient {
|
|
|
7182
7378
|
time: input.time,
|
|
7183
7379
|
id: input.id
|
|
7184
7380
|
});
|
|
7381
|
+
const matched = channelMatchesEvent(channel, event);
|
|
7382
|
+
return {
|
|
7383
|
+
channelId: channel.id,
|
|
7384
|
+
matched,
|
|
7385
|
+
event,
|
|
7386
|
+
filters: channel.filters,
|
|
7387
|
+
reason: matched ? undefined : channel.enabled ? "event did not match channel filters" : "channel is disabled"
|
|
7388
|
+
};
|
|
7389
|
+
}
|
|
7390
|
+
async testChannel(id, input = {}, options = {}) {
|
|
7391
|
+
const channel = await this.store.getChannel(id);
|
|
7392
|
+
if (!channel)
|
|
7393
|
+
throw new Error(`Channel not found: ${id}`);
|
|
7394
|
+
const match = await this.matchChannel(id, input);
|
|
7395
|
+
const event = match.event;
|
|
7396
|
+
if (options.honorFilters && !match.matched) {
|
|
7397
|
+
const timestamp = new Date().toISOString();
|
|
7398
|
+
const result2 = createDeliveryResult(event, channel, [{
|
|
7399
|
+
attempt: 1,
|
|
7400
|
+
status: "skipped",
|
|
7401
|
+
startedAt: timestamp,
|
|
7402
|
+
completedAt: timestamp,
|
|
7403
|
+
error: match.reason
|
|
7404
|
+
}]);
|
|
7405
|
+
result2.metadata = { reason: "filter_mismatch" };
|
|
7406
|
+
await this.store.appendDelivery(result2);
|
|
7407
|
+
return result2;
|
|
7408
|
+
}
|
|
7185
7409
|
const eventForChannel = await this.applyRedaction(event, channel);
|
|
7186
7410
|
const result = await this.deliverWithRetry(eventForChannel, channel);
|
|
7187
7411
|
await this.store.appendDelivery(result);
|
|
@@ -7281,6 +7505,90 @@ function normalizeRetryPolicy(policy) {
|
|
|
7281
7505
|
};
|
|
7282
7506
|
}
|
|
7283
7507
|
|
|
7508
|
+
// src/lib/shared-events.ts
|
|
7509
|
+
init_database();
|
|
7510
|
+
|
|
7511
|
+
// src/db/task-lists.ts
|
|
7512
|
+
init_types();
|
|
7513
|
+
init_database();
|
|
7514
|
+
function rowToTaskList(row) {
|
|
7515
|
+
return {
|
|
7516
|
+
...row,
|
|
7517
|
+
metadata: JSON.parse(row.metadata || "{}")
|
|
7518
|
+
};
|
|
7519
|
+
}
|
|
7520
|
+
function createTaskList(input, db) {
|
|
7521
|
+
const d = db || getDatabase();
|
|
7522
|
+
const id = uuid();
|
|
7523
|
+
const timestamp = now();
|
|
7524
|
+
const slug = input.slug || slugify(input.name);
|
|
7525
|
+
if (!input.project_id) {
|
|
7526
|
+
const existing = d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ?").get(slug);
|
|
7527
|
+
if (existing) {
|
|
7528
|
+
throw new Error(`Standalone task list with slug "${slug}" already exists`);
|
|
7529
|
+
}
|
|
7530
|
+
}
|
|
7531
|
+
d.run(`INSERT INTO task_lists (id, project_id, slug, name, description, metadata, created_at, updated_at)
|
|
7532
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.project_id || null, slug, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp, timestamp]);
|
|
7533
|
+
return getTaskList(id, d);
|
|
7534
|
+
}
|
|
7535
|
+
function getTaskList(id, db) {
|
|
7536
|
+
const d = db || getDatabase();
|
|
7537
|
+
const row = d.query("SELECT * FROM task_lists WHERE id = ?").get(id);
|
|
7538
|
+
return row ? rowToTaskList(row) : null;
|
|
7539
|
+
}
|
|
7540
|
+
function getTaskListBySlug(slug, projectId, db) {
|
|
7541
|
+
const d = db || getDatabase();
|
|
7542
|
+
let row;
|
|
7543
|
+
if (projectId) {
|
|
7544
|
+
row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id = ?").get(slug, projectId);
|
|
7545
|
+
} else {
|
|
7546
|
+
row = d.query("SELECT * FROM task_lists WHERE slug = ? AND project_id IS NULL").get(slug);
|
|
7547
|
+
}
|
|
7548
|
+
return row ? rowToTaskList(row) : null;
|
|
7549
|
+
}
|
|
7550
|
+
function listTaskLists(projectId, db) {
|
|
7551
|
+
const d = db || getDatabase();
|
|
7552
|
+
if (projectId) {
|
|
7553
|
+
return d.query("SELECT * FROM task_lists WHERE project_id = ? ORDER BY name").all(projectId).map(rowToTaskList);
|
|
7554
|
+
}
|
|
7555
|
+
return d.query("SELECT * FROM task_lists ORDER BY name").all().map(rowToTaskList);
|
|
7556
|
+
}
|
|
7557
|
+
function updateTaskList(id, input, db) {
|
|
7558
|
+
const d = db || getDatabase();
|
|
7559
|
+
const existing = getTaskList(id, d);
|
|
7560
|
+
if (!existing)
|
|
7561
|
+
throw new TaskListNotFoundError(id);
|
|
7562
|
+
const sets = ["updated_at = ?"];
|
|
7563
|
+
const params = [now()];
|
|
7564
|
+
if (input.name !== undefined) {
|
|
7565
|
+
sets.push("name = ?");
|
|
7566
|
+
params.push(input.name);
|
|
7567
|
+
}
|
|
7568
|
+
if (input.description !== undefined) {
|
|
7569
|
+
sets.push("description = ?");
|
|
7570
|
+
params.push(input.description);
|
|
7571
|
+
}
|
|
7572
|
+
if (input.metadata !== undefined) {
|
|
7573
|
+
sets.push("metadata = ?");
|
|
7574
|
+
params.push(JSON.stringify(input.metadata));
|
|
7575
|
+
}
|
|
7576
|
+
params.push(id);
|
|
7577
|
+
d.run(`UPDATE task_lists SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
7578
|
+
return getTaskList(id, d);
|
|
7579
|
+
}
|
|
7580
|
+
function deleteTaskList(id, db) {
|
|
7581
|
+
const d = db || getDatabase();
|
|
7582
|
+
return d.run("DELETE FROM task_lists WHERE id = ?", [id]).changes > 0;
|
|
7583
|
+
}
|
|
7584
|
+
function ensureTaskList(name, slug, projectId, db) {
|
|
7585
|
+
const d = db || getDatabase();
|
|
7586
|
+
const existing = getTaskListBySlug(slug, projectId, d);
|
|
7587
|
+
if (existing)
|
|
7588
|
+
return existing;
|
|
7589
|
+
return createTaskList({ name, slug, project_id: projectId }, d);
|
|
7590
|
+
}
|
|
7591
|
+
|
|
7284
7592
|
// src/lib/shared-events.ts
|
|
7285
7593
|
var SOURCE = "todos";
|
|
7286
7594
|
function taskEventData(task, extra = {}) {
|
|
@@ -7311,6 +7619,69 @@ function taskEventData(task, extra = {}) {
|
|
|
7311
7619
|
...extra
|
|
7312
7620
|
};
|
|
7313
7621
|
}
|
|
7622
|
+
function taskEventMetadata(task) {
|
|
7623
|
+
const metadata = {
|
|
7624
|
+
package: "@hasna/todos",
|
|
7625
|
+
todos_event_schema_version: 1,
|
|
7626
|
+
task_id: task.id,
|
|
7627
|
+
task_short_id: task.short_id,
|
|
7628
|
+
project_id: task.project_id,
|
|
7629
|
+
task_list_id: task.task_list_id,
|
|
7630
|
+
working_dir: task.working_dir
|
|
7631
|
+
};
|
|
7632
|
+
try {
|
|
7633
|
+
const project = task.project_id ? getProject(task.project_id) : null;
|
|
7634
|
+
const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
|
|
7635
|
+
if (project) {
|
|
7636
|
+
metadata.project_id = project.id;
|
|
7637
|
+
metadata.project_name = project.name;
|
|
7638
|
+
metadata.project_path = projectPath;
|
|
7639
|
+
metadata.project_canonical_path = project.path;
|
|
7640
|
+
metadata.project_default_task_list_slug = project.task_list_id;
|
|
7641
|
+
metadata.root_project_id = inferRootProjectId(project);
|
|
7642
|
+
} else if (projectPath) {
|
|
7643
|
+
metadata.project_path = projectPath;
|
|
7644
|
+
metadata.project_canonical_path = projectPath;
|
|
7645
|
+
}
|
|
7646
|
+
if (projectPath) {
|
|
7647
|
+
metadata.project_kind = classifyProjectKind(projectPath);
|
|
7648
|
+
metadata.project_is_worktree = isWorktreePath(projectPath);
|
|
7649
|
+
if (typeof task.metadata.route_enabled === "boolean") {
|
|
7650
|
+
metadata.route_enabled = task.metadata.route_enabled;
|
|
7651
|
+
}
|
|
7652
|
+
metadata.working_dir = task.working_dir ?? projectPath;
|
|
7653
|
+
}
|
|
7654
|
+
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;
|
|
7655
|
+
if (taskList) {
|
|
7656
|
+
metadata.task_list_id = taskList.id;
|
|
7657
|
+
metadata.task_list_slug = taskList.slug;
|
|
7658
|
+
metadata.task_list_name = taskList.name;
|
|
7659
|
+
metadata.task_list_project_id = taskList.project_id;
|
|
7660
|
+
metadata.task_list_is_project_default = Boolean(project?.task_list_id && taskList.slug === project.task_list_id);
|
|
7661
|
+
}
|
|
7662
|
+
} catch {}
|
|
7663
|
+
return metadata;
|
|
7664
|
+
}
|
|
7665
|
+
function classifyProjectKind(path) {
|
|
7666
|
+
return path.includes("/hasna/opensource/") ? "open-source" : "unknown";
|
|
7667
|
+
}
|
|
7668
|
+
function isWorktreePath(path) {
|
|
7669
|
+
return path.includes("/.codewith/worktrees/") || path.includes("/.worktrees/");
|
|
7670
|
+
}
|
|
7671
|
+
function inferRootProjectId(project) {
|
|
7672
|
+
return isWorktreePath(project.path) ? null : project.id;
|
|
7673
|
+
}
|
|
7674
|
+
function readMachineLocalPath(project) {
|
|
7675
|
+
const machineId = process.env["TODOS_MACHINE_ID"];
|
|
7676
|
+
if (!machineId)
|
|
7677
|
+
return null;
|
|
7678
|
+
try {
|
|
7679
|
+
const row = getDatabase().query("SELECT path FROM project_machine_paths WHERE project_id = ? AND machine_id = ?").get(project.id, machineId);
|
|
7680
|
+
return row?.path ?? null;
|
|
7681
|
+
} catch {
|
|
7682
|
+
return null;
|
|
7683
|
+
}
|
|
7684
|
+
}
|
|
7314
7685
|
async function emitSharedTaskEvent(input) {
|
|
7315
7686
|
const data = taskEventData(input.task, input.data);
|
|
7316
7687
|
await new EventsClient().emit({
|
|
@@ -7321,12 +7692,7 @@ async function emitSharedTaskEvent(input) {
|
|
|
7321
7692
|
message: input.message ?? `${input.type}: ${input.task.title}`,
|
|
7322
7693
|
data,
|
|
7323
7694
|
dedupeKey: input.dedupeKey ?? `${input.type}:${input.task.id}:${input.task.version}`,
|
|
7324
|
-
metadata:
|
|
7325
|
-
package: "@hasna/todos",
|
|
7326
|
-
task_id: input.task.id,
|
|
7327
|
-
project_id: input.task.project_id,
|
|
7328
|
-
task_list_id: input.task.task_list_id
|
|
7329
|
-
}
|
|
7695
|
+
metadata: taskEventMetadata(input.task)
|
|
7330
7696
|
}, { deliver: true, dedupe: true });
|
|
7331
7697
|
}
|
|
7332
7698
|
function emitSharedTaskEventQuiet(input) {
|
|
@@ -7689,6 +8055,17 @@ function replaceTaskTags(taskId, tags, db) {
|
|
|
7689
8055
|
db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
|
|
7690
8056
|
insertTaskTags(taskId, tags, db);
|
|
7691
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
|
+
}
|
|
7692
8069
|
function createTask(input, db) {
|
|
7693
8070
|
const d = db || getDatabase();
|
|
7694
8071
|
const timestamp = now();
|
|
@@ -7871,6 +8248,7 @@ function listTasks(filter = {}, db) {
|
|
|
7871
8248
|
params.push(filter.task_type);
|
|
7872
8249
|
}
|
|
7873
8250
|
}
|
|
8251
|
+
addMetadataConditions(filter.metadata, conditions, params);
|
|
7874
8252
|
const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
|
|
7875
8253
|
if (filter.cursor) {
|
|
7876
8254
|
try {
|
|
@@ -7895,6 +8273,54 @@ function listTasks(filter = {}, db) {
|
|
|
7895
8273
|
const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC${limitClause}`).all(...params);
|
|
7896
8274
|
return rows.map(rowToTask);
|
|
7897
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
|
+
}
|
|
7898
8324
|
function countTasks(filter = {}, db) {
|
|
7899
8325
|
const d = db || getDatabase();
|
|
7900
8326
|
const conditions = [];
|
|
@@ -7958,6 +8384,7 @@ function countTasks(filter = {}, db) {
|
|
|
7958
8384
|
conditions.push("task_list_id = ?");
|
|
7959
8385
|
params.push(filter.task_list_id);
|
|
7960
8386
|
}
|
|
8387
|
+
addMetadataConditions(filter.metadata, conditions, params);
|
|
7961
8388
|
if (!filter.include_archived) {
|
|
7962
8389
|
conditions.push("archived_at IS NULL");
|
|
7963
8390
|
}
|
|
@@ -8008,6 +8435,10 @@ function updateTask(id, input, db) {
|
|
|
8008
8435
|
sets.push("assigned_to = ?");
|
|
8009
8436
|
params.push(input.assigned_to);
|
|
8010
8437
|
}
|
|
8438
|
+
if (input.working_dir !== undefined) {
|
|
8439
|
+
sets.push("working_dir = ?");
|
|
8440
|
+
params.push(input.working_dir);
|
|
8441
|
+
}
|
|
8011
8442
|
if (input.tags !== undefined) {
|
|
8012
8443
|
sets.push("tags = ?");
|
|
8013
8444
|
params.push(JSON.stringify(input.tags));
|
|
@@ -8096,6 +8527,8 @@ function updateTask(id, input, db) {
|
|
|
8096
8527
|
logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
|
|
8097
8528
|
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
|
|
8098
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);
|
|
8099
8532
|
if (input.approved_by !== undefined)
|
|
8100
8533
|
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
8101
8534
|
const updatedTask = {
|
|
@@ -8131,6 +8564,10 @@ function updateTask(id, input, db) {
|
|
|
8131
8564
|
if (input.approved_by !== undefined) {
|
|
8132
8565
|
emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
|
|
8133
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 });
|
|
8134
8571
|
return updatedTask;
|
|
8135
8572
|
}
|
|
8136
8573
|
function deleteTask(id, db) {
|
|
@@ -10826,6 +11263,7 @@ function getTaskTraceability(taskId, db) {
|
|
|
10826
11263
|
|
|
10827
11264
|
// src/db/task-runs.ts
|
|
10828
11265
|
init_redaction();
|
|
11266
|
+
var LOOP_RUN_TRANSACTION_SCHEMA_VERSION = "todos.loop_run_transaction.v1";
|
|
10829
11267
|
function parseObject(value) {
|
|
10830
11268
|
if (!value)
|
|
10831
11269
|
return {};
|
|
@@ -10848,6 +11286,72 @@ function rowToArtifact(row) {
|
|
|
10848
11286
|
function getRunRow(runId, db) {
|
|
10849
11287
|
return db.query("SELECT * FROM task_runs WHERE id = ?").get(runId);
|
|
10850
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
|
+
}
|
|
10851
11355
|
function resolveTaskRunId(idOrPrefix, db) {
|
|
10852
11356
|
const d = db || getDatabase();
|
|
10853
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}%`);
|
|
@@ -10866,7 +11370,7 @@ function startTaskRun(input, db) {
|
|
|
10866
11370
|
const d = db || getDatabase();
|
|
10867
11371
|
if (!getTask(input.task_id, d))
|
|
10868
11372
|
throw new TaskNotFoundError(input.task_id);
|
|
10869
|
-
const id = uuid();
|
|
11373
|
+
const id = input.id ?? uuid();
|
|
10870
11374
|
const timestamp = input.started_at || now();
|
|
10871
11375
|
if (input.claim && input.agent_id) {
|
|
10872
11376
|
startTask(input.task_id, input.agent_id, d);
|
|
@@ -10904,6 +11408,97 @@ function startTaskRun(input, db) {
|
|
|
10904
11408
|
emitLocalEventHooksQuiet({ type: "run.started", payload: { id: run.id, task_id: run.task_id, agent_id: run.agent_id, title: run.title } });
|
|
10905
11409
|
return run;
|
|
10906
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
|
+
}
|
|
10907
11502
|
function addTaskRunEvent(input, db) {
|
|
10908
11503
|
const d = db || getDatabase();
|
|
10909
11504
|
const runId = resolveTaskRunId(input.run_id, d);
|
|
@@ -11083,6 +11678,66 @@ function finishTaskRun(input, db) {
|
|
|
11083
11678
|
});
|
|
11084
11679
|
return updated;
|
|
11085
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
|
+
}
|
|
11086
11741
|
function listTaskRuns(taskId, db) {
|
|
11087
11742
|
const d = db || getDatabase();
|
|
11088
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();
|
|
@@ -11644,7 +12299,7 @@ function rowToTask2(row) {
|
|
|
11644
12299
|
requires_approval: Boolean(row.requires_approval)
|
|
11645
12300
|
};
|
|
11646
12301
|
}
|
|
11647
|
-
function
|
|
12302
|
+
function rowToTaskList2(row) {
|
|
11648
12303
|
return { ...row, metadata: parseJsonObject3(row.metadata) };
|
|
11649
12304
|
}
|
|
11650
12305
|
function rowWithMetadata(row) {
|
|
@@ -11680,7 +12335,7 @@ function createLocalBridgeBundle(options = {}, db) {
|
|
|
11680
12335
|
const project = options.project_id ? d.query("SELECT * FROM projects WHERE id = ?").get(options.project_id) : null;
|
|
11681
12336
|
const data = redactValue({
|
|
11682
12337
|
projects: options.project_id ? project ? [project] : [] : d.query("SELECT * FROM projects ORDER BY name").all(),
|
|
11683
|
-
task_lists: (options.project_id ? d.query("SELECT * FROM task_lists WHERE project_id = ? ORDER BY name").all(options.project_id) : d.query("SELECT * FROM task_lists ORDER BY name").all()).map(
|
|
12338
|
+
task_lists: (options.project_id ? d.query("SELECT * FROM task_lists WHERE project_id = ? ORDER BY name").all(options.project_id) : d.query("SELECT * FROM task_lists ORDER BY name").all()).map(rowToTaskList2),
|
|
11684
12339
|
plans: options.project_id ? d.query("SELECT * FROM plans WHERE project_id = ? ORDER BY created_at").all(options.project_id) : d.query("SELECT * FROM plans ORDER BY created_at").all(),
|
|
11685
12340
|
tasks: queryByTaskIds(d, "SELECT * FROM tasks WHERE id IN (__TASK_IDS__) ORDER BY created_at", taskIds).map(rowToTask2),
|
|
11686
12341
|
task_dependencies: queryByTaskIds(d, "SELECT task_id, depends_on, external_project_id, external_task_id FROM task_dependencies WHERE task_id IN (__TASK_IDS__) ORDER BY task_id, depends_on", taskIds),
|
|
@@ -17263,7 +17918,7 @@ function countMovedStandaloneVerifications(db, duplicateId) {
|
|
|
17263
17918
|
const rows = db.query("SELECT command FROM task_verifications WHERE task_id = ?").all(duplicateId);
|
|
17264
17919
|
return rows.filter((row) => !generatedCommands.has(row.command)).length;
|
|
17265
17920
|
}
|
|
17266
|
-
function
|
|
17921
|
+
function mergeTaskMetadata2(primary, duplicate, input, mergedAt) {
|
|
17267
17922
|
const mergedDuplicates = Array.isArray(primary.metadata["merged_duplicates"]) ? [...primary.metadata["merged_duplicates"]] : [];
|
|
17268
17923
|
mergedDuplicates.push({
|
|
17269
17924
|
id: duplicate.id,
|
|
@@ -17324,7 +17979,7 @@ function mergeDuplicateTask(input, db) {
|
|
|
17324
17979
|
updateTask(primary.id, {
|
|
17325
17980
|
version: primary.version,
|
|
17326
17981
|
tags: mergedTags,
|
|
17327
|
-
metadata:
|
|
17982
|
+
metadata: mergeTaskMetadata2(primary, duplicate, input, mergedAt),
|
|
17328
17983
|
description: mergeTaskDescription(primary, duplicate) ?? undefined
|
|
17329
17984
|
}, d);
|
|
17330
17985
|
moved.comments = updateRows(d, "task_comments", "task_id", duplicate.id, primary.id);
|
|
@@ -19795,6 +20450,33 @@ var TODOS_API_ROUTES = [
|
|
|
19795
20450
|
tags: ["tasks", "mutation"],
|
|
19796
20451
|
stability: "stable"
|
|
19797
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
|
+
},
|
|
19798
20480
|
{
|
|
19799
20481
|
id: "tasks.read",
|
|
19800
20482
|
method: "GET",
|