@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/index.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,
|
|
@@ -3681,6 +3765,7 @@ var MCP_TOOL_GROUPS = {
|
|
|
3681
3765
|
"add_task_run_event",
|
|
3682
3766
|
"add_task_run_file",
|
|
3683
3767
|
"acknowledge_handoff",
|
|
3768
|
+
"begin_task_run_transaction",
|
|
3684
3769
|
"build_local_report",
|
|
3685
3770
|
"cancel_agent_run_dispatch",
|
|
3686
3771
|
"finish_task_run",
|
|
@@ -3728,6 +3813,7 @@ var MCP_TOOL_GROUPS = {
|
|
|
3728
3813
|
"list_local_snapshots",
|
|
3729
3814
|
"list_retrospectives",
|
|
3730
3815
|
"list_risks",
|
|
3816
|
+
"list_task_findings",
|
|
3731
3817
|
"list_task_runs",
|
|
3732
3818
|
"list_verification_providers",
|
|
3733
3819
|
"merge_duplicate_task",
|
|
@@ -3736,6 +3822,7 @@ var MCP_TOOL_GROUPS = {
|
|
|
3736
3822
|
"remove_review_routing_rule",
|
|
3737
3823
|
"restore_local_backup",
|
|
3738
3824
|
"retry_agent_run_dispatch",
|
|
3825
|
+
"resolve_missing_task_findings",
|
|
3739
3826
|
"resolve_mentions",
|
|
3740
3827
|
"run_next_agent_dispatch",
|
|
3741
3828
|
"search_knowledge_records",
|
|
@@ -3778,9 +3865,17 @@ var MCP_TOOL_GROUPS = {
|
|
|
3778
3865
|
"unlock_file",
|
|
3779
3866
|
"unwatch_task",
|
|
3780
3867
|
"update_comment",
|
|
3868
|
+
"upsert_task_finding",
|
|
3781
3869
|
"update_risk",
|
|
3782
3870
|
"watch_task"
|
|
3783
3871
|
],
|
|
3872
|
+
loops: [
|
|
3873
|
+
"begin_task_run_transaction",
|
|
3874
|
+
"finish_task_run",
|
|
3875
|
+
"list_task_findings",
|
|
3876
|
+
"resolve_missing_task_findings",
|
|
3877
|
+
"upsert_task_finding"
|
|
3878
|
+
],
|
|
3784
3879
|
agents: [
|
|
3785
3880
|
"auto_assign_task",
|
|
3786
3881
|
"delete_agent",
|
|
@@ -3862,7 +3957,7 @@ var MCP_TOOL_GROUPS = {
|
|
|
3862
3957
|
maintenance: ["extract_todos", "get_sla_breaches", "notify_upcoming_deadlines", "run_doctor", "score_task", "watch_source_todos"]
|
|
3863
3958
|
};
|
|
3864
3959
|
var MCP_PROFILE_GROUPS = {
|
|
3865
|
-
minimal: ["core"],
|
|
3960
|
+
minimal: ["core", "loops"],
|
|
3866
3961
|
core: ["core"],
|
|
3867
3962
|
standard: ["core", "tasks", "projects", "resources", "agents", "metadata"],
|
|
3868
3963
|
agent: ["core", "tasks", "projects", "resources"],
|
|
@@ -4935,6 +5030,92 @@ var TODOS_JSON_CONTRACTS = [
|
|
|
4935
5030
|
},
|
|
4936
5031
|
optional: {}
|
|
4937
5032
|
}),
|
|
5033
|
+
contract({
|
|
5034
|
+
id: "loop_run_transaction",
|
|
5035
|
+
name: "Loop Run Transaction",
|
|
5036
|
+
description: "Compact local result for idempotent loop run begin/finish transactions.",
|
|
5037
|
+
surfaces: ["cli", "mcp", "sdk"],
|
|
5038
|
+
stability: "stable",
|
|
5039
|
+
required: {
|
|
5040
|
+
schema_version: field("string", "Result schema version."),
|
|
5041
|
+
local_only: field("boolean", "Always true; loop run transactions use local state."),
|
|
5042
|
+
dry_run: field("boolean", "True when no run ledger mutation was applied."),
|
|
5043
|
+
processed_at: isoDateField,
|
|
5044
|
+
action: field("string", "preview, created, matched, finished, or conflict."),
|
|
5045
|
+
key: field("string", "Stable idempotency key used to dedupe the transaction."),
|
|
5046
|
+
run: field(["object", "null"], "Compact run summary or null for create previews.", true),
|
|
5047
|
+
warnings: field("array", "Non-fatal warnings such as terminal-status conflicts."),
|
|
5048
|
+
commands: field("array", "Follow-up CLI commands for agents and operators.")
|
|
5049
|
+
},
|
|
5050
|
+
optional: {}
|
|
5051
|
+
}),
|
|
5052
|
+
contract({
|
|
5053
|
+
id: "task_finding",
|
|
5054
|
+
name: "Task Finding",
|
|
5055
|
+
description: "Compact local finding record deduped by task and fingerprint.",
|
|
5056
|
+
surfaces: ["cli", "mcp", "sdk"],
|
|
5057
|
+
stability: "stable",
|
|
5058
|
+
required: {
|
|
5059
|
+
schema_version: field("string", "Finding schema version."),
|
|
5060
|
+
id: idField,
|
|
5061
|
+
task_id: idField,
|
|
5062
|
+
run_id: field(["string", "null"], "Optional run ledger ID.", true),
|
|
5063
|
+
fingerprint: field("string", "Stable finding fingerprint scoped to the task."),
|
|
5064
|
+
title: field("string", "Short redacted finding title."),
|
|
5065
|
+
severity: field("string", "low, medium, high, or critical."),
|
|
5066
|
+
status: field("string", "open, resolved, or ignored."),
|
|
5067
|
+
source: field(["string", "null"], "Optional loop/tool source.", true),
|
|
5068
|
+
summary: field(["string", "null"], "Bounded redacted finding summary.", true),
|
|
5069
|
+
artifact_path: field(["string", "null"], "Local artifact path/reference; raw content is not included.", true),
|
|
5070
|
+
first_seen_at: isoDateField,
|
|
5071
|
+
last_seen_at: isoDateField,
|
|
5072
|
+
resolved_at: field(["string", "null"], "Resolution timestamp when closed.", true),
|
|
5073
|
+
metadata_keys: field("array", "Sorted metadata keys; metadata values are intentionally omitted in compact output.")
|
|
5074
|
+
},
|
|
5075
|
+
optional: {}
|
|
5076
|
+
}),
|
|
5077
|
+
contract({
|
|
5078
|
+
id: "task_finding_upsert",
|
|
5079
|
+
name: "Task Finding Upsert Result",
|
|
5080
|
+
description: "Local-only dry-run or applied result from idempotently upserting a task finding.",
|
|
5081
|
+
surfaces: ["cli", "mcp", "sdk"],
|
|
5082
|
+
stability: "stable",
|
|
5083
|
+
required: {
|
|
5084
|
+
schema_version: field("string", "Result schema version."),
|
|
5085
|
+
local_only: field("boolean", "Always true; finding upserts use local state."),
|
|
5086
|
+
dry_run: field("boolean", "True when no finding row was created or updated."),
|
|
5087
|
+
processed_at: isoDateField,
|
|
5088
|
+
action: field("string", "preview, created, matched, updated, or reopened."),
|
|
5089
|
+
fingerprint: field("string", "Normalized finding fingerprint."),
|
|
5090
|
+
finding: field(["object", "null"], "Compact finding summary or null for create previews.", true),
|
|
5091
|
+
warnings: field("array", "Non-fatal warnings.")
|
|
5092
|
+
},
|
|
5093
|
+
optional: {}
|
|
5094
|
+
}),
|
|
5095
|
+
contract({
|
|
5096
|
+
id: "task_finding_resolve_missing",
|
|
5097
|
+
name: "Task Finding Resolve Missing Result",
|
|
5098
|
+
description: "Local-only dry-run or applied result from resolving open findings absent from the latest loop finding set.",
|
|
5099
|
+
surfaces: ["cli", "mcp", "sdk"],
|
|
5100
|
+
stability: "stable",
|
|
5101
|
+
required: {
|
|
5102
|
+
schema_version: field("string", "Result schema version."),
|
|
5103
|
+
local_only: field("boolean", "Always true; finding resolution uses local state."),
|
|
5104
|
+
dry_run: field("boolean", "True when no finding rows were changed."),
|
|
5105
|
+
processed_at: isoDateField,
|
|
5106
|
+
action: field("string", "preview, resolved, ignored, or noop."),
|
|
5107
|
+
task_id: idField,
|
|
5108
|
+
source: field(["string", "null"], "Optional source scope.", true),
|
|
5109
|
+
run_id: field(["string", "null"], "Optional run ledger ID used for audit metadata.", true),
|
|
5110
|
+
present_fingerprint_count: field("integer", "Number of fingerprints supplied as still present."),
|
|
5111
|
+
candidate_count: field("integer", "Open findings missing from the supplied set."),
|
|
5112
|
+
changed_count: field("integer", "Rows resolved or ignored by the applied transaction."),
|
|
5113
|
+
omitted_count: field("integer", "Matching findings omitted from bounded output."),
|
|
5114
|
+
findings: field("array", "Bounded compact finding summaries."),
|
|
5115
|
+
warnings: field("array", "Non-fatal warnings.")
|
|
5116
|
+
},
|
|
5117
|
+
optional: {}
|
|
5118
|
+
}),
|
|
4938
5119
|
contract({
|
|
4939
5120
|
id: "verification_provider",
|
|
4940
5121
|
name: "Verification Provider",
|
|
@@ -6640,6 +6821,7 @@ var LOCAL_EVENT_TYPES = [
|
|
|
6640
6821
|
"task.blocked",
|
|
6641
6822
|
"task.started",
|
|
6642
6823
|
"task.completed",
|
|
6824
|
+
"task.updated",
|
|
6643
6825
|
"task.due",
|
|
6644
6826
|
"task.due_soon",
|
|
6645
6827
|
"task.failed",
|
|
@@ -6878,7 +7060,7 @@ async function testLocalEventHook(name, input) {
|
|
|
6878
7060
|
return emitLocalEventHooks({ ...input, hooks: [hook] });
|
|
6879
7061
|
}
|
|
6880
7062
|
|
|
6881
|
-
// node_modules/.bun/@hasna+events@0.1.
|
|
7063
|
+
// node_modules/.bun/@hasna+events@0.1.10/node_modules/@hasna/events/dist/index.js
|
|
6882
7064
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
6883
7065
|
import { existsSync as existsSync6 } from "fs";
|
|
6884
7066
|
import { homedir } from "os";
|
|
@@ -6925,14 +7107,40 @@ function matchRecord(input, matcher) {
|
|
|
6925
7107
|
return true;
|
|
6926
7108
|
return Object.entries(matcher).every(([path, expected]) => {
|
|
6927
7109
|
const actual = getPathValue(input, path);
|
|
6928
|
-
|
|
6929
|
-
return matchString(actual === undefined ? undefined : String(actual), expected, {
|
|
6930
|
-
segmentSafe: path.endsWith("_path") || path.endsWith(".path")
|
|
6931
|
-
});
|
|
6932
|
-
}
|
|
6933
|
-
return actual === expected;
|
|
7110
|
+
return matchField(actual, expected, path);
|
|
6934
7111
|
});
|
|
6935
7112
|
}
|
|
7113
|
+
function matchField(actual, expected, path) {
|
|
7114
|
+
if (isNegativeMatcher(expected)) {
|
|
7115
|
+
return !matchPositiveField(actual, expected.not, path);
|
|
7116
|
+
}
|
|
7117
|
+
return matchPositiveField(actual, expected, path);
|
|
7118
|
+
}
|
|
7119
|
+
function matchPositiveField(actual, expected, path) {
|
|
7120
|
+
if (typeof expected === "string" || Array.isArray(expected)) {
|
|
7121
|
+
return stringCandidates(actual).some((candidate) => matchString(candidate, expected, {
|
|
7122
|
+
segmentSafe: path.endsWith("_path") || path.endsWith(".path")
|
|
7123
|
+
}));
|
|
7124
|
+
}
|
|
7125
|
+
if (Array.isArray(actual)) {
|
|
7126
|
+
return actual.some((item) => item === expected);
|
|
7127
|
+
}
|
|
7128
|
+
return actual === expected;
|
|
7129
|
+
}
|
|
7130
|
+
function stringCandidates(actual) {
|
|
7131
|
+
if (actual === undefined)
|
|
7132
|
+
return [];
|
|
7133
|
+
if (Array.isArray(actual)) {
|
|
7134
|
+
return actual.flatMap((item) => isPrimitiveFieldValue(item) ? [String(item)] : []);
|
|
7135
|
+
}
|
|
7136
|
+
return [String(actual)];
|
|
7137
|
+
}
|
|
7138
|
+
function isPrimitiveFieldValue(value) {
|
|
7139
|
+
return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
|
|
7140
|
+
}
|
|
7141
|
+
function isNegativeMatcher(value) {
|
|
7142
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value) && "not" in value);
|
|
7143
|
+
}
|
|
6936
7144
|
function eventMatchesFilter(event, filter) {
|
|
6937
7145
|
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);
|
|
6938
7146
|
}
|
|
@@ -7539,9 +7747,66 @@ function taskEventData(task, extra = {}) {
|
|
|
7539
7747
|
started_at: task.started_at,
|
|
7540
7748
|
completed_at: task.completed_at,
|
|
7541
7749
|
due_at: task.due_at,
|
|
7750
|
+
requires_approval: task.requires_approval,
|
|
7751
|
+
approved_by: task.approved_by,
|
|
7752
|
+
approved_at: task.approved_at,
|
|
7542
7753
|
...extra
|
|
7543
7754
|
};
|
|
7544
7755
|
}
|
|
7756
|
+
function booleanField(value) {
|
|
7757
|
+
if (typeof value === "boolean")
|
|
7758
|
+
return value;
|
|
7759
|
+
if (typeof value === "number") {
|
|
7760
|
+
if (value === 1)
|
|
7761
|
+
return true;
|
|
7762
|
+
if (value === 0)
|
|
7763
|
+
return false;
|
|
7764
|
+
}
|
|
7765
|
+
if (typeof value === "string") {
|
|
7766
|
+
const normalized = value.trim().toLowerCase();
|
|
7767
|
+
if (["true", "1", "yes", "on"].includes(normalized))
|
|
7768
|
+
return true;
|
|
7769
|
+
if (["false", "0", "no", "off"].includes(normalized))
|
|
7770
|
+
return false;
|
|
7771
|
+
}
|
|
7772
|
+
return;
|
|
7773
|
+
}
|
|
7774
|
+
function objectField(value) {
|
|
7775
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
7776
|
+
}
|
|
7777
|
+
function firstBoolean(records, keys) {
|
|
7778
|
+
for (const record of records) {
|
|
7779
|
+
for (const key of keys) {
|
|
7780
|
+
const value = booleanField(record[key]);
|
|
7781
|
+
if (value !== undefined)
|
|
7782
|
+
return value;
|
|
7783
|
+
}
|
|
7784
|
+
}
|
|
7785
|
+
return;
|
|
7786
|
+
}
|
|
7787
|
+
function routingAutomationMetadata(task) {
|
|
7788
|
+
const automation = objectField(task.metadata.automation);
|
|
7789
|
+
const records = [task.metadata];
|
|
7790
|
+
if (automation)
|
|
7791
|
+
records.push(automation);
|
|
7792
|
+
const result = {};
|
|
7793
|
+
const aliases = [
|
|
7794
|
+
["allowed", ["allowed", "automation_allowed", "automationAllowed"]],
|
|
7795
|
+
["no_auto", ["no_auto", "noAuto"]],
|
|
7796
|
+
["manual", ["manual"]],
|
|
7797
|
+
["manual_required", ["manual_required", "manualRequired"]],
|
|
7798
|
+
["requires_approval", ["requires_approval", "requiresApproval"]],
|
|
7799
|
+
["approval_required", ["approval_required", "approvalRequired"]]
|
|
7800
|
+
];
|
|
7801
|
+
for (const [canonical, keys] of aliases) {
|
|
7802
|
+
const value = firstBoolean(records, keys);
|
|
7803
|
+
if (value !== undefined)
|
|
7804
|
+
result[canonical] = value;
|
|
7805
|
+
}
|
|
7806
|
+
if (task.requires_approval)
|
|
7807
|
+
result.requires_approval = true;
|
|
7808
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
7809
|
+
}
|
|
7545
7810
|
function taskEventMetadata(task) {
|
|
7546
7811
|
const metadata = {
|
|
7547
7812
|
package: "@hasna/todos",
|
|
@@ -7552,6 +7817,14 @@ function taskEventMetadata(task) {
|
|
|
7552
7817
|
task_list_id: task.task_list_id,
|
|
7553
7818
|
working_dir: task.working_dir
|
|
7554
7819
|
};
|
|
7820
|
+
const routeEnabled = booleanField(task.metadata.route_enabled);
|
|
7821
|
+
if (routeEnabled !== undefined) {
|
|
7822
|
+
metadata.route_enabled = routeEnabled;
|
|
7823
|
+
}
|
|
7824
|
+
const automation = routingAutomationMetadata(task);
|
|
7825
|
+
if (automation) {
|
|
7826
|
+
metadata.automation = automation;
|
|
7827
|
+
}
|
|
7555
7828
|
try {
|
|
7556
7829
|
const project = task.project_id ? getProject(task.project_id) : null;
|
|
7557
7830
|
const projectPath = project ? readMachineLocalPath(project) ?? project.path : task.working_dir;
|
|
@@ -7569,9 +7842,6 @@ function taskEventMetadata(task) {
|
|
|
7569
7842
|
if (projectPath) {
|
|
7570
7843
|
metadata.project_kind = classifyProjectKind(projectPath);
|
|
7571
7844
|
metadata.project_is_worktree = isWorktreePath(projectPath);
|
|
7572
|
-
if (typeof task.metadata.route_enabled === "boolean") {
|
|
7573
|
-
metadata.route_enabled = task.metadata.route_enabled;
|
|
7574
|
-
}
|
|
7575
7845
|
metadata.working_dir = task.working_dir ?? projectPath;
|
|
7576
7846
|
}
|
|
7577
7847
|
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;
|
|
@@ -7978,6 +8248,17 @@ function replaceTaskTags(taskId, tags, db) {
|
|
|
7978
8248
|
db.run("DELETE FROM task_tags WHERE task_id = ?", [taskId]);
|
|
7979
8249
|
insertTaskTags(taskId, tags, db);
|
|
7980
8250
|
}
|
|
8251
|
+
function addMetadataConditions(metadata, conditions, params) {
|
|
8252
|
+
if (!metadata)
|
|
8253
|
+
return;
|
|
8254
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
8255
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(key)) {
|
|
8256
|
+
throw new Error(`Invalid metadata filter key: ${key}`);
|
|
8257
|
+
}
|
|
8258
|
+
conditions.push(`json_extract(metadata, '$."${key}"') = ?`);
|
|
8259
|
+
params.push(value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? value : JSON.stringify(value));
|
|
8260
|
+
}
|
|
8261
|
+
}
|
|
7981
8262
|
function createTask(input, db) {
|
|
7982
8263
|
const d = db || getDatabase();
|
|
7983
8264
|
const timestamp = now();
|
|
@@ -8160,6 +8441,7 @@ function listTasks(filter = {}, db) {
|
|
|
8160
8441
|
params.push(filter.task_type);
|
|
8161
8442
|
}
|
|
8162
8443
|
}
|
|
8444
|
+
addMetadataConditions(filter.metadata, conditions, params);
|
|
8163
8445
|
const PRIORITY_RANK = `CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END`;
|
|
8164
8446
|
if (filter.cursor) {
|
|
8165
8447
|
try {
|
|
@@ -8184,6 +8466,54 @@ function listTasks(filter = {}, db) {
|
|
|
8184
8466
|
const rows = d.query(`SELECT * FROM tasks ${where} ORDER BY ${PRIORITY_RANK}, created_at DESC${limitClause}`).all(...params);
|
|
8185
8467
|
return rows.map(rowToTask);
|
|
8186
8468
|
}
|
|
8469
|
+
function getTaskByFingerprint(fingerprint, db) {
|
|
8470
|
+
const tasks = listTasks({ metadata: { fingerprint }, limit: 1 }, db);
|
|
8471
|
+
return tasks[0] ?? null;
|
|
8472
|
+
}
|
|
8473
|
+
function mergeTaskMetadata(current, next, fingerprint) {
|
|
8474
|
+
return {
|
|
8475
|
+
...current,
|
|
8476
|
+
...next ?? {},
|
|
8477
|
+
fingerprint
|
|
8478
|
+
};
|
|
8479
|
+
}
|
|
8480
|
+
function upsertTaskByFingerprint(input, db) {
|
|
8481
|
+
const d = db || getDatabase();
|
|
8482
|
+
const fingerprint = input.fingerprint.trim();
|
|
8483
|
+
if (!fingerprint)
|
|
8484
|
+
throw new Error("fingerprint is required");
|
|
8485
|
+
const existing = getTaskByFingerprint(fingerprint, d);
|
|
8486
|
+
const metadata = mergeTaskMetadata(existing?.metadata ?? {}, input.metadata, fingerprint);
|
|
8487
|
+
if (!existing) {
|
|
8488
|
+
const task2 = createTask({ ...input, metadata }, d);
|
|
8489
|
+
return { task: task2, created: true };
|
|
8490
|
+
}
|
|
8491
|
+
const task = updateTask(existing.id, {
|
|
8492
|
+
version: existing.version,
|
|
8493
|
+
title: input.title,
|
|
8494
|
+
description: input.description,
|
|
8495
|
+
status: input.status,
|
|
8496
|
+
priority: input.priority,
|
|
8497
|
+
project_id: input.project_id,
|
|
8498
|
+
assigned_to: input.assigned_to,
|
|
8499
|
+
working_dir: input.working_dir,
|
|
8500
|
+
plan_id: input.plan_id,
|
|
8501
|
+
task_list_id: input.task_list_id,
|
|
8502
|
+
tags: input.tags,
|
|
8503
|
+
metadata,
|
|
8504
|
+
due_at: input.due_at,
|
|
8505
|
+
estimated_minutes: input.estimated_minutes,
|
|
8506
|
+
sla_minutes: input.sla_minutes,
|
|
8507
|
+
confidence: input.confidence,
|
|
8508
|
+
retry_count: input.retry_count,
|
|
8509
|
+
max_retries: input.max_retries,
|
|
8510
|
+
retry_after: input.retry_after,
|
|
8511
|
+
requires_approval: input.requires_approval,
|
|
8512
|
+
recurrence_rule: input.recurrence_rule,
|
|
8513
|
+
task_type: input.task_type
|
|
8514
|
+
}, d);
|
|
8515
|
+
return { task, created: false };
|
|
8516
|
+
}
|
|
8187
8517
|
function countTasks(filter = {}, db) {
|
|
8188
8518
|
const d = db || getDatabase();
|
|
8189
8519
|
const conditions = [];
|
|
@@ -8247,6 +8577,7 @@ function countTasks(filter = {}, db) {
|
|
|
8247
8577
|
conditions.push("task_list_id = ?");
|
|
8248
8578
|
params.push(filter.task_list_id);
|
|
8249
8579
|
}
|
|
8580
|
+
addMetadataConditions(filter.metadata, conditions, params);
|
|
8250
8581
|
if (!filter.include_archived) {
|
|
8251
8582
|
conditions.push("archived_at IS NULL");
|
|
8252
8583
|
}
|
|
@@ -8297,6 +8628,10 @@ function updateTask(id, input, db) {
|
|
|
8297
8628
|
sets.push("assigned_to = ?");
|
|
8298
8629
|
params.push(input.assigned_to);
|
|
8299
8630
|
}
|
|
8631
|
+
if (input.working_dir !== undefined) {
|
|
8632
|
+
sets.push("working_dir = ?");
|
|
8633
|
+
params.push(input.working_dir);
|
|
8634
|
+
}
|
|
8300
8635
|
if (input.tags !== undefined) {
|
|
8301
8636
|
sets.push("tags = ?");
|
|
8302
8637
|
params.push(JSON.stringify(input.tags));
|
|
@@ -8385,6 +8720,8 @@ function updateTask(id, input, db) {
|
|
|
8385
8720
|
logTaskChange(id, "update", "title", task.title, input.title, agentId, d);
|
|
8386
8721
|
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
|
|
8387
8722
|
logTaskChange(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
|
|
8723
|
+
if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
|
|
8724
|
+
logTaskChange(id, "update", "working_dir", task.working_dir, input.working_dir, agentId, d);
|
|
8388
8725
|
if (input.approved_by !== undefined)
|
|
8389
8726
|
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
8390
8727
|
const updatedTask = {
|
|
@@ -8420,6 +8757,10 @@ function updateTask(id, input, db) {
|
|
|
8420
8757
|
if (input.approved_by !== undefined) {
|
|
8421
8758
|
emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
|
|
8422
8759
|
}
|
|
8760
|
+
const updatePayload = taskEventData(updatedTask);
|
|
8761
|
+
dispatchWebhook2("task.updated", updatePayload, d).catch(() => {});
|
|
8762
|
+
emitLocalEventHooksQuiet({ type: "task.updated", payload: updatePayload });
|
|
8763
|
+
emitSharedTaskEventQuiet({ type: "task.updated", task: updatedTask });
|
|
8423
8764
|
return updatedTask;
|
|
8424
8765
|
}
|
|
8425
8766
|
function deleteTask(id, db) {
|
|
@@ -11115,6 +11456,7 @@ function getTaskTraceability(taskId, db) {
|
|
|
11115
11456
|
|
|
11116
11457
|
// src/db/task-runs.ts
|
|
11117
11458
|
init_redaction();
|
|
11459
|
+
var LOOP_RUN_TRANSACTION_SCHEMA_VERSION = "todos.loop_run_transaction.v1";
|
|
11118
11460
|
function parseObject(value) {
|
|
11119
11461
|
if (!value)
|
|
11120
11462
|
return {};
|
|
@@ -11137,6 +11479,72 @@ function rowToArtifact(row) {
|
|
|
11137
11479
|
function getRunRow(runId, db) {
|
|
11138
11480
|
return db.query("SELECT * FROM task_runs WHERE id = ?").get(runId);
|
|
11139
11481
|
}
|
|
11482
|
+
function normalizeTransactionKey(input) {
|
|
11483
|
+
const key = (input.key || input.loop_run_id || input.loop_id || "").trim();
|
|
11484
|
+
if (!key)
|
|
11485
|
+
throw new Error("idempotent run transactions require --key, --loop-run-id, or --loop-id");
|
|
11486
|
+
return key.toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 240);
|
|
11487
|
+
}
|
|
11488
|
+
function loopTransactionMetadata(record) {
|
|
11489
|
+
const value = record.metadata["loop_transaction"];
|
|
11490
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
11491
|
+
}
|
|
11492
|
+
function runKey(record) {
|
|
11493
|
+
const tx = loopTransactionMetadata(record);
|
|
11494
|
+
const key = tx["idempotency_key"] ?? record.metadata["idempotency_key"];
|
|
11495
|
+
return typeof key === "string" ? key : null;
|
|
11496
|
+
}
|
|
11497
|
+
function loopId(record) {
|
|
11498
|
+
const tx = loopTransactionMetadata(record);
|
|
11499
|
+
const value = tx["loop_id"] ?? record.metadata["loop_id"];
|
|
11500
|
+
return typeof value === "string" ? value : null;
|
|
11501
|
+
}
|
|
11502
|
+
function loopRunId(record) {
|
|
11503
|
+
const tx = loopTransactionMetadata(record);
|
|
11504
|
+
const value = tx["loop_run_id"] ?? record.metadata["loop_run_id"];
|
|
11505
|
+
return typeof value === "string" ? value : null;
|
|
11506
|
+
}
|
|
11507
|
+
function getTaskRunTransactionByKey(key, taskId, db) {
|
|
11508
|
+
if (taskId) {
|
|
11509
|
+
return db.query("SELECT * FROM task_run_transactions WHERE task_id = ? AND key = ?").get(taskId, key);
|
|
11510
|
+
}
|
|
11511
|
+
const rows = db.query("SELECT * FROM task_run_transactions WHERE key = ? ORDER BY created_at DESC LIMIT 2").all(key);
|
|
11512
|
+
if (rows.length > 1)
|
|
11513
|
+
throw new Error(`Run transaction key is ambiguous across tasks: ${key}. Pass task_id.`);
|
|
11514
|
+
return rows[0] ?? null;
|
|
11515
|
+
}
|
|
11516
|
+
function summarizeTaskRun(run) {
|
|
11517
|
+
return {
|
|
11518
|
+
id: run.id,
|
|
11519
|
+
task_id: run.task_id,
|
|
11520
|
+
agent_id: run.agent_id,
|
|
11521
|
+
title: run.title,
|
|
11522
|
+
status: run.status,
|
|
11523
|
+
summary: run.summary,
|
|
11524
|
+
idempotency_key: runKey(run),
|
|
11525
|
+
loop_id: loopId(run),
|
|
11526
|
+
loop_run_id: loopRunId(run),
|
|
11527
|
+
metadata_keys: Object.keys(run.metadata).sort(),
|
|
11528
|
+
started_at: run.started_at,
|
|
11529
|
+
completed_at: run.completed_at,
|
|
11530
|
+
updated_at: run.updated_at
|
|
11531
|
+
};
|
|
11532
|
+
}
|
|
11533
|
+
function findTaskRunByTransactionKey(key, taskId, db) {
|
|
11534
|
+
const d = db || getDatabase();
|
|
11535
|
+
const normalized = normalizeTransactionKey({ key });
|
|
11536
|
+
const transaction = getTaskRunTransactionByKey(normalized, taskId, d);
|
|
11537
|
+
if (transaction?.run_id)
|
|
11538
|
+
return getTaskRun(transaction.run_id, d);
|
|
11539
|
+
return null;
|
|
11540
|
+
}
|
|
11541
|
+
function loopRunCommands(run, key) {
|
|
11542
|
+
return [
|
|
11543
|
+
run ? `todos runs show ${run.id.slice(0, 8)}` : "todos runs list",
|
|
11544
|
+
run ? `todos findings list --task ${run.task_id.slice(0, 8)} --json` : "todos findings list --json",
|
|
11545
|
+
`todos runs begin <task-id> --key ${key} --apply --json`
|
|
11546
|
+
];
|
|
11547
|
+
}
|
|
11140
11548
|
function resolveTaskRunId(idOrPrefix, db) {
|
|
11141
11549
|
const d = db || getDatabase();
|
|
11142
11550
|
const rows = d.query("SELECT id FROM task_runs WHERE id = ? OR id LIKE ? ORDER BY created_at DESC LIMIT 2").all(idOrPrefix, `${idOrPrefix}%`);
|
|
@@ -11155,7 +11563,7 @@ function startTaskRun(input, db) {
|
|
|
11155
11563
|
const d = db || getDatabase();
|
|
11156
11564
|
if (!getTask(input.task_id, d))
|
|
11157
11565
|
throw new TaskNotFoundError(input.task_id);
|
|
11158
|
-
const id = uuid();
|
|
11566
|
+
const id = input.id ?? uuid();
|
|
11159
11567
|
const timestamp = input.started_at || now();
|
|
11160
11568
|
if (input.claim && input.agent_id) {
|
|
11161
11569
|
startTask(input.task_id, input.agent_id, d);
|
|
@@ -11193,6 +11601,97 @@ function startTaskRun(input, db) {
|
|
|
11193
11601
|
emitLocalEventHooksQuiet({ type: "run.started", payload: { id: run.id, task_id: run.task_id, agent_id: run.agent_id, title: run.title } });
|
|
11194
11602
|
return run;
|
|
11195
11603
|
}
|
|
11604
|
+
function beginTaskRunTransaction(input, db) {
|
|
11605
|
+
const d = db || getDatabase();
|
|
11606
|
+
if (!getTask(input.task_id, d))
|
|
11607
|
+
throw new TaskNotFoundError(input.task_id);
|
|
11608
|
+
const timestamp = input.started_at || now();
|
|
11609
|
+
const key = normalizeTransactionKey(input);
|
|
11610
|
+
const existing = findTaskRunByTransactionKey(key, input.task_id, d);
|
|
11611
|
+
const dryRun = !input.apply;
|
|
11612
|
+
if (existing) {
|
|
11613
|
+
return {
|
|
11614
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
11615
|
+
local_only: true,
|
|
11616
|
+
dry_run: dryRun,
|
|
11617
|
+
processed_at: timestamp,
|
|
11618
|
+
action: "matched",
|
|
11619
|
+
key,
|
|
11620
|
+
run: summarizeTaskRun(existing),
|
|
11621
|
+
warnings: existing.status === "running" ? [] : [`matched ${existing.status} run`],
|
|
11622
|
+
commands: loopRunCommands(existing, key)
|
|
11623
|
+
};
|
|
11624
|
+
}
|
|
11625
|
+
if (dryRun) {
|
|
11626
|
+
return {
|
|
11627
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
11628
|
+
local_only: true,
|
|
11629
|
+
dry_run: true,
|
|
11630
|
+
processed_at: timestamp,
|
|
11631
|
+
action: "preview",
|
|
11632
|
+
key,
|
|
11633
|
+
run: null,
|
|
11634
|
+
warnings: [],
|
|
11635
|
+
commands: loopRunCommands(null, key)
|
|
11636
|
+
};
|
|
11637
|
+
}
|
|
11638
|
+
const metadata = redactValue({
|
|
11639
|
+
...input.metadata || {},
|
|
11640
|
+
loop_transaction: {
|
|
11641
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
11642
|
+
idempotency_key: key,
|
|
11643
|
+
loop_id: input.loop_id ?? null,
|
|
11644
|
+
loop_run_id: input.loop_run_id ?? null,
|
|
11645
|
+
first_seen_at: timestamp
|
|
11646
|
+
},
|
|
11647
|
+
idempotency_key: key
|
|
11648
|
+
});
|
|
11649
|
+
const created = d.transaction(() => {
|
|
11650
|
+
d.run(`INSERT OR IGNORE INTO task_run_transactions (
|
|
11651
|
+
id, task_id, run_id, key, loop_id, loop_run_id, metadata, created_at, updated_at
|
|
11652
|
+
) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?)`, [
|
|
11653
|
+
uuid(),
|
|
11654
|
+
input.task_id,
|
|
11655
|
+
key,
|
|
11656
|
+
input.loop_id ?? null,
|
|
11657
|
+
input.loop_run_id ?? null,
|
|
11658
|
+
JSON.stringify(metadata),
|
|
11659
|
+
timestamp,
|
|
11660
|
+
timestamp
|
|
11661
|
+
]);
|
|
11662
|
+
const transaction = getTaskRunTransactionByKey(key, input.task_id, d);
|
|
11663
|
+
if (!transaction)
|
|
11664
|
+
throw new Error(`Could not create run transaction for key: ${key}`);
|
|
11665
|
+
if (transaction.run_id) {
|
|
11666
|
+
const existingRun = getTaskRun(transaction.run_id, d);
|
|
11667
|
+
if (existingRun)
|
|
11668
|
+
return { run: existingRun, action: "matched" };
|
|
11669
|
+
}
|
|
11670
|
+
const run = startTaskRun({
|
|
11671
|
+
id: uuid(),
|
|
11672
|
+
task_id: input.task_id,
|
|
11673
|
+
agent_id: input.agent_id,
|
|
11674
|
+
title: input.title,
|
|
11675
|
+
summary: input.summary,
|
|
11676
|
+
metadata,
|
|
11677
|
+
claim: input.claim,
|
|
11678
|
+
started_at: timestamp
|
|
11679
|
+
}, d);
|
|
11680
|
+
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]);
|
|
11681
|
+
return { run, action: "created" };
|
|
11682
|
+
})();
|
|
11683
|
+
return {
|
|
11684
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
11685
|
+
local_only: true,
|
|
11686
|
+
dry_run: false,
|
|
11687
|
+
processed_at: timestamp,
|
|
11688
|
+
action: created.action,
|
|
11689
|
+
key,
|
|
11690
|
+
run: summarizeTaskRun(created.run),
|
|
11691
|
+
warnings: [],
|
|
11692
|
+
commands: loopRunCommands(created.run, key)
|
|
11693
|
+
};
|
|
11694
|
+
}
|
|
11196
11695
|
function addTaskRunEvent(input, db) {
|
|
11197
11696
|
const d = db || getDatabase();
|
|
11198
11697
|
const runId = resolveTaskRunId(input.run_id, d);
|
|
@@ -11372,6 +11871,66 @@ function finishTaskRun(input, db) {
|
|
|
11372
11871
|
});
|
|
11373
11872
|
return updated;
|
|
11374
11873
|
}
|
|
11874
|
+
function finishTaskRunTransaction(input, db) {
|
|
11875
|
+
const d = db || getDatabase();
|
|
11876
|
+
const timestamp = input.completed_at || now();
|
|
11877
|
+
const status = input.status || "completed";
|
|
11878
|
+
const key = input.key ? normalizeTransactionKey({ key: input.key }) : "";
|
|
11879
|
+
const run = input.run_id ? getTaskRun(resolveTaskRunId(input.run_id, d), d) : key ? findTaskRunByTransactionKey(key, input.task_id, d) : null;
|
|
11880
|
+
if (!run) {
|
|
11881
|
+
throw new Error(input.run_id ? `Run not found: ${input.run_id}` : "runs finish requires a run id or --key");
|
|
11882
|
+
}
|
|
11883
|
+
if (input.task_id && run.task_id !== input.task_id) {
|
|
11884
|
+
throw new Error(`Run ${run.id} belongs to task ${run.task_id}, not ${input.task_id}`);
|
|
11885
|
+
}
|
|
11886
|
+
const resolvedKey = key || runKey(run) || run.id;
|
|
11887
|
+
const dryRun = input.apply === false;
|
|
11888
|
+
if (run.status !== "running") {
|
|
11889
|
+
const conflict = run.status !== status;
|
|
11890
|
+
return {
|
|
11891
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
11892
|
+
local_only: true,
|
|
11893
|
+
dry_run: dryRun,
|
|
11894
|
+
processed_at: timestamp,
|
|
11895
|
+
action: conflict ? "conflict" : "matched",
|
|
11896
|
+
key: resolvedKey,
|
|
11897
|
+
run: summarizeTaskRun(run),
|
|
11898
|
+
warnings: conflict ? [`run is already ${run.status}; requested ${status}`] : [],
|
|
11899
|
+
commands: loopRunCommands(run, resolvedKey)
|
|
11900
|
+
};
|
|
11901
|
+
}
|
|
11902
|
+
if (dryRun) {
|
|
11903
|
+
return {
|
|
11904
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
11905
|
+
local_only: true,
|
|
11906
|
+
dry_run: true,
|
|
11907
|
+
processed_at: timestamp,
|
|
11908
|
+
action: "preview",
|
|
11909
|
+
key: resolvedKey,
|
|
11910
|
+
run: summarizeTaskRun({ ...run, status, summary: input.summary ?? run.summary, completed_at: timestamp, updated_at: timestamp }),
|
|
11911
|
+
warnings: [],
|
|
11912
|
+
commands: loopRunCommands(run, resolvedKey)
|
|
11913
|
+
};
|
|
11914
|
+
}
|
|
11915
|
+
const finished = finishTaskRun({
|
|
11916
|
+
run_id: run.id,
|
|
11917
|
+
status,
|
|
11918
|
+
summary: input.summary,
|
|
11919
|
+
agent_id: input.agent_id,
|
|
11920
|
+
completed_at: timestamp
|
|
11921
|
+
}, d);
|
|
11922
|
+
return {
|
|
11923
|
+
schema_version: LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
11924
|
+
local_only: true,
|
|
11925
|
+
dry_run: false,
|
|
11926
|
+
processed_at: timestamp,
|
|
11927
|
+
action: "finished",
|
|
11928
|
+
key: resolvedKey,
|
|
11929
|
+
run: summarizeTaskRun(finished),
|
|
11930
|
+
warnings: [],
|
|
11931
|
+
commands: loopRunCommands(finished, resolvedKey)
|
|
11932
|
+
};
|
|
11933
|
+
}
|
|
11375
11934
|
function listTaskRuns(taskId, db) {
|
|
11376
11935
|
const d = db || getDatabase();
|
|
11377
11936
|
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();
|
|
@@ -17552,7 +18111,7 @@ function countMovedStandaloneVerifications(db, duplicateId) {
|
|
|
17552
18111
|
const rows = db.query("SELECT command FROM task_verifications WHERE task_id = ?").all(duplicateId);
|
|
17553
18112
|
return rows.filter((row) => !generatedCommands.has(row.command)).length;
|
|
17554
18113
|
}
|
|
17555
|
-
function
|
|
18114
|
+
function mergeTaskMetadata2(primary, duplicate, input, mergedAt) {
|
|
17556
18115
|
const mergedDuplicates = Array.isArray(primary.metadata["merged_duplicates"]) ? [...primary.metadata["merged_duplicates"]] : [];
|
|
17557
18116
|
mergedDuplicates.push({
|
|
17558
18117
|
id: duplicate.id,
|
|
@@ -17613,7 +18172,7 @@ function mergeDuplicateTask(input, db) {
|
|
|
17613
18172
|
updateTask(primary.id, {
|
|
17614
18173
|
version: primary.version,
|
|
17615
18174
|
tags: mergedTags,
|
|
17616
|
-
metadata:
|
|
18175
|
+
metadata: mergeTaskMetadata2(primary, duplicate, input, mergedAt),
|
|
17617
18176
|
description: mergeTaskDescription(primary, duplicate) ?? undefined
|
|
17618
18177
|
}, d);
|
|
17619
18178
|
moved.comments = updateRows(d, "task_comments", "task_id", duplicate.id, primary.id);
|
|
@@ -20084,6 +20643,33 @@ var TODOS_API_ROUTES = [
|
|
|
20084
20643
|
tags: ["tasks", "mutation"],
|
|
20085
20644
|
stability: "stable"
|
|
20086
20645
|
},
|
|
20646
|
+
{
|
|
20647
|
+
id: "tasks.upsert",
|
|
20648
|
+
method: "POST",
|
|
20649
|
+
path: "/api/tasks/upsert",
|
|
20650
|
+
description: "Create or update a task by stable metadata fingerprint, merging metadata on updates.",
|
|
20651
|
+
auth: "optional-api-key",
|
|
20652
|
+
requestSchema: {
|
|
20653
|
+
type: "object",
|
|
20654
|
+
properties: {
|
|
20655
|
+
fingerprint: { type: "string" },
|
|
20656
|
+
title: { type: "string" },
|
|
20657
|
+
description: { type: "string" },
|
|
20658
|
+
priority: { type: "string", enum: TASK_PRIORITIES },
|
|
20659
|
+
status: { type: "string", enum: TASK_STATUSES },
|
|
20660
|
+
project_id: { type: "string" },
|
|
20661
|
+
task_list_id: { type: "string" },
|
|
20662
|
+
working_dir: { type: "string" },
|
|
20663
|
+
tags: { type: "array", items: { type: "string" } },
|
|
20664
|
+
metadata: objectSchema
|
|
20665
|
+
},
|
|
20666
|
+
required: ["fingerprint", "title"],
|
|
20667
|
+
additionalProperties: true
|
|
20668
|
+
},
|
|
20669
|
+
responseSchema: objectSchema,
|
|
20670
|
+
tags: ["tasks", "mutation", "dedupe"],
|
|
20671
|
+
stability: "stable"
|
|
20672
|
+
},
|
|
20087
20673
|
{
|
|
20088
20674
|
id: "tasks.read",
|
|
20089
20675
|
method: "GET",
|
|
@@ -28962,6 +29548,7 @@ var READ_ONLY_TOOLS = new Set([
|
|
|
28962
29548
|
"describe_tools",
|
|
28963
29549
|
"search_tools",
|
|
28964
29550
|
"inspect_git_commit",
|
|
29551
|
+
"list_task_findings",
|
|
28965
29552
|
"scan_text_for_secrets",
|
|
28966
29553
|
"check_workspace_permission",
|
|
28967
29554
|
"check_sandbox_command",
|
|
@@ -28987,7 +29574,12 @@ var MINIMAL_TOOLS = new Set([
|
|
|
28987
29574
|
"bootstrap",
|
|
28988
29575
|
"get_tasks_changed_since",
|
|
28989
29576
|
"heartbeat",
|
|
28990
|
-
"release_agent"
|
|
29577
|
+
"release_agent",
|
|
29578
|
+
"begin_task_run_transaction",
|
|
29579
|
+
"finish_task_run",
|
|
29580
|
+
"upsert_task_finding",
|
|
29581
|
+
"list_task_findings",
|
|
29582
|
+
"resolve_missing_task_findings"
|
|
28991
29583
|
]);
|
|
28992
29584
|
var AGENT_SAFE_TOOLS = new Set([
|
|
28993
29585
|
...READ_ONLY_TOOLS,
|
|
@@ -41825,6 +42417,353 @@ async function dispatchToMultiple(input, opts = {}, db) {
|
|
|
41825
42417
|
}
|
|
41826
42418
|
return dispatches;
|
|
41827
42419
|
}
|
|
42420
|
+
// src/db/findings.ts
|
|
42421
|
+
init_redaction();
|
|
42422
|
+
init_types();
|
|
42423
|
+
init_database();
|
|
42424
|
+
var TASK_FINDING_SCHEMA_VERSION = "todos.task_finding.v1";
|
|
42425
|
+
var TASK_FINDING_UPSERT_SCHEMA_VERSION = "todos.task_finding_upsert.v1";
|
|
42426
|
+
var TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION = "todos.task_finding_resolve_missing.v1";
|
|
42427
|
+
var SEVERITIES2 = new Set(["low", "medium", "high", "critical"]);
|
|
42428
|
+
var STATUSES = new Set(["open", "resolved", "ignored"]);
|
|
42429
|
+
function parseObject4(value) {
|
|
42430
|
+
if (!value)
|
|
42431
|
+
return {};
|
|
42432
|
+
try {
|
|
42433
|
+
const parsed = JSON.parse(value);
|
|
42434
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
42435
|
+
} catch {
|
|
42436
|
+
return {};
|
|
42437
|
+
}
|
|
42438
|
+
}
|
|
42439
|
+
function normalizeKey(value) {
|
|
42440
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
42441
|
+
}
|
|
42442
|
+
function normalizeFingerprint(value) {
|
|
42443
|
+
const normalized = normalizeKey(value);
|
|
42444
|
+
if (!normalized)
|
|
42445
|
+
throw new Error("finding fingerprint is required");
|
|
42446
|
+
return normalized.slice(0, 240);
|
|
42447
|
+
}
|
|
42448
|
+
function normalizeSeverity2(value) {
|
|
42449
|
+
const normalized = normalizeKey(value || "medium");
|
|
42450
|
+
if (SEVERITIES2.has(normalized))
|
|
42451
|
+
return normalized;
|
|
42452
|
+
if (/^(p0|blocker|urgent|highest)$/.test(normalized))
|
|
42453
|
+
return "critical";
|
|
42454
|
+
if (/^(p1|major)$/.test(normalized))
|
|
42455
|
+
return "high";
|
|
42456
|
+
if (/^(p3|minor|info)$/.test(normalized))
|
|
42457
|
+
return "low";
|
|
42458
|
+
return "medium";
|
|
42459
|
+
}
|
|
42460
|
+
function normalizeStatus(value) {
|
|
42461
|
+
const normalized = normalizeKey(value || "open");
|
|
42462
|
+
if (STATUSES.has(normalized))
|
|
42463
|
+
return normalized;
|
|
42464
|
+
if (normalized === "closed" || normalized === "fixed")
|
|
42465
|
+
return "resolved";
|
|
42466
|
+
return "open";
|
|
42467
|
+
}
|
|
42468
|
+
function normalizeResolutionStatus(value) {
|
|
42469
|
+
const status = normalizeStatus(value || "resolved");
|
|
42470
|
+
if (status === "open")
|
|
42471
|
+
throw new Error("resolve-missing status must be resolved or ignored");
|
|
42472
|
+
return status;
|
|
42473
|
+
}
|
|
42474
|
+
function redactOptional(value, max = 2000) {
|
|
42475
|
+
if (!value)
|
|
42476
|
+
return null;
|
|
42477
|
+
const redacted = redactEvidenceText(value).trim();
|
|
42478
|
+
if (!redacted)
|
|
42479
|
+
return null;
|
|
42480
|
+
return redacted.length > max ? `${redacted.slice(0, max - 3)}...` : redacted;
|
|
42481
|
+
}
|
|
42482
|
+
function rowToFinding(row) {
|
|
42483
|
+
return {
|
|
42484
|
+
schema_version: TASK_FINDING_SCHEMA_VERSION,
|
|
42485
|
+
...row,
|
|
42486
|
+
severity: normalizeSeverity2(row.severity),
|
|
42487
|
+
status: normalizeStatus(row.status),
|
|
42488
|
+
metadata: parseObject4(row.metadata)
|
|
42489
|
+
};
|
|
42490
|
+
}
|
|
42491
|
+
function compactFinding(finding2) {
|
|
42492
|
+
return {
|
|
42493
|
+
schema_version: TASK_FINDING_SCHEMA_VERSION,
|
|
42494
|
+
id: finding2.id,
|
|
42495
|
+
task_id: finding2.task_id,
|
|
42496
|
+
run_id: finding2.run_id,
|
|
42497
|
+
fingerprint: finding2.fingerprint,
|
|
42498
|
+
title: finding2.title,
|
|
42499
|
+
severity: finding2.severity,
|
|
42500
|
+
status: finding2.status,
|
|
42501
|
+
source: finding2.source,
|
|
42502
|
+
summary: finding2.summary,
|
|
42503
|
+
artifact_path: finding2.artifact_path,
|
|
42504
|
+
first_seen_at: finding2.first_seen_at,
|
|
42505
|
+
last_seen_at: finding2.last_seen_at,
|
|
42506
|
+
resolved_at: finding2.resolved_at,
|
|
42507
|
+
metadata_keys: Object.keys(finding2.metadata).sort()
|
|
42508
|
+
};
|
|
42509
|
+
}
|
|
42510
|
+
function previewFinding(existing, next, timestamp2) {
|
|
42511
|
+
return {
|
|
42512
|
+
...existing,
|
|
42513
|
+
run_id: next.run_id,
|
|
42514
|
+
title: next.title,
|
|
42515
|
+
severity: next.severity,
|
|
42516
|
+
status: next.status,
|
|
42517
|
+
source: next.source,
|
|
42518
|
+
summary: next.summary,
|
|
42519
|
+
artifact_path: next.artifact_path,
|
|
42520
|
+
metadata: next.metadata,
|
|
42521
|
+
last_seen_at: timestamp2,
|
|
42522
|
+
resolved_at: next.status === "open" ? null : existing.resolved_at || timestamp2,
|
|
42523
|
+
updated_at: timestamp2
|
|
42524
|
+
};
|
|
42525
|
+
}
|
|
42526
|
+
function upsertAction(existing, next) {
|
|
42527
|
+
if (sameFinding(existing, next))
|
|
42528
|
+
return "matched";
|
|
42529
|
+
return existing.status !== "open" && next.status === "open" ? "reopened" : "updated";
|
|
42530
|
+
}
|
|
42531
|
+
function resolveRunForTask(runId, taskId, db) {
|
|
42532
|
+
if (!runId)
|
|
42533
|
+
return null;
|
|
42534
|
+
const resolved = resolveTaskRunId(runId, db);
|
|
42535
|
+
const run = getTaskRun(resolved, db);
|
|
42536
|
+
if (!run)
|
|
42537
|
+
throw new Error(`Run not found: ${runId}`);
|
|
42538
|
+
if (run.task_id !== taskId)
|
|
42539
|
+
throw new Error(`Run ${resolved} belongs to task ${run.task_id}, not ${taskId}`);
|
|
42540
|
+
return resolved;
|
|
42541
|
+
}
|
|
42542
|
+
function getFindingByFingerprint(taskId, fingerprint4, db) {
|
|
42543
|
+
const row = db.query("SELECT * FROM task_findings WHERE task_id = ? AND fingerprint = ?").get(taskId, fingerprint4);
|
|
42544
|
+
return row ? rowToFinding(row) : null;
|
|
42545
|
+
}
|
|
42546
|
+
function assertTask(taskId, db) {
|
|
42547
|
+
if (!getTask(taskId, db))
|
|
42548
|
+
throw new TaskNotFoundError(taskId);
|
|
42549
|
+
}
|
|
42550
|
+
function nextFinding(input, db) {
|
|
42551
|
+
const fingerprint4 = normalizeFingerprint(input.fingerprint);
|
|
42552
|
+
const title = redactOptional(input.title, 300);
|
|
42553
|
+
if (!title)
|
|
42554
|
+
throw new Error("finding title is required");
|
|
42555
|
+
return {
|
|
42556
|
+
fingerprint: fingerprint4,
|
|
42557
|
+
run_id: resolveRunForTask(input.run_id, input.task_id, db),
|
|
42558
|
+
title,
|
|
42559
|
+
severity: normalizeSeverity2(input.severity),
|
|
42560
|
+
status: normalizeStatus(input.status),
|
|
42561
|
+
source: redactOptional(input.source, 120),
|
|
42562
|
+
summary: redactOptional(input.summary, 2000),
|
|
42563
|
+
artifact_path: redactOptional(input.artifact_path, 1000),
|
|
42564
|
+
metadata: redactValue(input.metadata || {})
|
|
42565
|
+
};
|
|
42566
|
+
}
|
|
42567
|
+
function sameFinding(left, right) {
|
|
42568
|
+
return left.run_id === right.run_id && left.title === right.title && left.severity === right.severity && left.status === right.status && left.source === right.source && left.summary === right.summary && left.artifact_path === right.artifact_path && JSON.stringify(left.metadata) === JSON.stringify(right.metadata);
|
|
42569
|
+
}
|
|
42570
|
+
function upsertTaskFinding(input, db) {
|
|
42571
|
+
const d = db || getDatabase();
|
|
42572
|
+
assertTask(input.task_id, d);
|
|
42573
|
+
const timestamp2 = input.observed_at || now();
|
|
42574
|
+
const warnings = [];
|
|
42575
|
+
const next = nextFinding(input, d);
|
|
42576
|
+
const existing = getFindingByFingerprint(input.task_id, next.fingerprint, d);
|
|
42577
|
+
const dryRun = !input.apply;
|
|
42578
|
+
if (dryRun) {
|
|
42579
|
+
const action2 = existing ? upsertAction(existing, next) : "preview";
|
|
42580
|
+
return {
|
|
42581
|
+
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
42582
|
+
local_only: true,
|
|
42583
|
+
dry_run: true,
|
|
42584
|
+
processed_at: timestamp2,
|
|
42585
|
+
action: action2,
|
|
42586
|
+
fingerprint: next.fingerprint,
|
|
42587
|
+
finding: existing ? compactFinding(previewFinding(existing, next, timestamp2)) : null,
|
|
42588
|
+
warnings
|
|
42589
|
+
};
|
|
42590
|
+
}
|
|
42591
|
+
if (!existing) {
|
|
42592
|
+
const id = uuid();
|
|
42593
|
+
d.run(`INSERT INTO task_findings (
|
|
42594
|
+
id, task_id, run_id, fingerprint, title, severity, status, source, summary, artifact_path,
|
|
42595
|
+
metadata, first_seen_at, last_seen_at, resolved_at, created_at, updated_at
|
|
42596
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
42597
|
+
id,
|
|
42598
|
+
input.task_id,
|
|
42599
|
+
next.run_id,
|
|
42600
|
+
next.fingerprint,
|
|
42601
|
+
next.title,
|
|
42602
|
+
next.severity,
|
|
42603
|
+
next.status,
|
|
42604
|
+
next.source,
|
|
42605
|
+
next.summary,
|
|
42606
|
+
next.artifact_path,
|
|
42607
|
+
JSON.stringify(next.metadata),
|
|
42608
|
+
timestamp2,
|
|
42609
|
+
timestamp2,
|
|
42610
|
+
next.status === "open" ? null : timestamp2,
|
|
42611
|
+
timestamp2,
|
|
42612
|
+
timestamp2
|
|
42613
|
+
]);
|
|
42614
|
+
return {
|
|
42615
|
+
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
42616
|
+
local_only: true,
|
|
42617
|
+
dry_run: false,
|
|
42618
|
+
processed_at: timestamp2,
|
|
42619
|
+
action: "created",
|
|
42620
|
+
fingerprint: next.fingerprint,
|
|
42621
|
+
finding: compactFinding(getFindingByFingerprint(input.task_id, next.fingerprint, d)),
|
|
42622
|
+
warnings
|
|
42623
|
+
};
|
|
42624
|
+
}
|
|
42625
|
+
if (sameFinding(existing, next)) {
|
|
42626
|
+
return {
|
|
42627
|
+
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
42628
|
+
local_only: true,
|
|
42629
|
+
dry_run: false,
|
|
42630
|
+
processed_at: timestamp2,
|
|
42631
|
+
action: "matched",
|
|
42632
|
+
fingerprint: next.fingerprint,
|
|
42633
|
+
finding: compactFinding(existing),
|
|
42634
|
+
warnings
|
|
42635
|
+
};
|
|
42636
|
+
}
|
|
42637
|
+
const action = upsertAction(existing, next);
|
|
42638
|
+
d.run(`UPDATE task_findings SET
|
|
42639
|
+
run_id = ?, title = ?, severity = ?, status = ?, source = ?, summary = ?, artifact_path = ?,
|
|
42640
|
+
metadata = ?, last_seen_at = ?, resolved_at = ?, updated_at = ?
|
|
42641
|
+
WHERE id = ?`, [
|
|
42642
|
+
next.run_id,
|
|
42643
|
+
next.title,
|
|
42644
|
+
next.severity,
|
|
42645
|
+
next.status,
|
|
42646
|
+
next.source,
|
|
42647
|
+
next.summary,
|
|
42648
|
+
next.artifact_path,
|
|
42649
|
+
JSON.stringify(next.metadata),
|
|
42650
|
+
timestamp2,
|
|
42651
|
+
next.status === "open" ? null : existing.resolved_at || timestamp2,
|
|
42652
|
+
timestamp2,
|
|
42653
|
+
existing.id
|
|
42654
|
+
]);
|
|
42655
|
+
return {
|
|
42656
|
+
schema_version: TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
42657
|
+
local_only: true,
|
|
42658
|
+
dry_run: false,
|
|
42659
|
+
processed_at: timestamp2,
|
|
42660
|
+
action,
|
|
42661
|
+
fingerprint: next.fingerprint,
|
|
42662
|
+
finding: compactFinding(getFindingByFingerprint(input.task_id, next.fingerprint, d)),
|
|
42663
|
+
warnings
|
|
42664
|
+
};
|
|
42665
|
+
}
|
|
42666
|
+
function listTaskFindings(filter = {}, db) {
|
|
42667
|
+
const d = db || getDatabase();
|
|
42668
|
+
const conditions = ["1=1"];
|
|
42669
|
+
const params = [];
|
|
42670
|
+
if (filter.task_id) {
|
|
42671
|
+
conditions.push("task_id = ?");
|
|
42672
|
+
params.push(filter.task_id);
|
|
42673
|
+
}
|
|
42674
|
+
if (filter.run_id) {
|
|
42675
|
+
conditions.push("run_id = ?");
|
|
42676
|
+
params.push(resolveTaskRunId(filter.run_id, d));
|
|
42677
|
+
}
|
|
42678
|
+
if (filter.status) {
|
|
42679
|
+
conditions.push("status = ?");
|
|
42680
|
+
params.push(normalizeStatus(filter.status));
|
|
42681
|
+
}
|
|
42682
|
+
if (filter.source) {
|
|
42683
|
+
conditions.push("source = ?");
|
|
42684
|
+
params.push(redactOptional(filter.source, 120));
|
|
42685
|
+
}
|
|
42686
|
+
const limit = Math.min(Math.max(Math.floor(filter.limit ?? 50), 1), 500);
|
|
42687
|
+
const rows = d.query(`SELECT * FROM task_findings WHERE ${conditions.join(" AND ")} ORDER BY updated_at DESC, created_at DESC LIMIT ?`).all(...[...params, limit]);
|
|
42688
|
+
return rows.map(rowToFinding);
|
|
42689
|
+
}
|
|
42690
|
+
function listCompactTaskFindings(filter = {}, db) {
|
|
42691
|
+
return listTaskFindings(filter, db).map(compactFinding);
|
|
42692
|
+
}
|
|
42693
|
+
function resolveMissingTaskFindings(input, db) {
|
|
42694
|
+
const d = db || getDatabase();
|
|
42695
|
+
assertTask(input.task_id, d);
|
|
42696
|
+
const timestamp2 = input.resolved_at || now();
|
|
42697
|
+
const status = normalizeResolutionStatus(input.status);
|
|
42698
|
+
const runId = resolveRunForTask(input.run_id, input.task_id, d);
|
|
42699
|
+
const present = new Set(input.fingerprints.map(normalizeFingerprint));
|
|
42700
|
+
const warnings = [];
|
|
42701
|
+
const conditions = ["task_id = ?", "status = 'open'"];
|
|
42702
|
+
const params = [input.task_id];
|
|
42703
|
+
if (input.source) {
|
|
42704
|
+
conditions.push("source = ?");
|
|
42705
|
+
params.push(redactOptional(input.source, 120));
|
|
42706
|
+
}
|
|
42707
|
+
const candidates = d.query(`SELECT * FROM task_findings WHERE ${conditions.join(" AND ")} ORDER BY updated_at DESC, created_at DESC`).all(...params).map(rowToFinding).filter((finding2) => !present.has(finding2.fingerprint));
|
|
42708
|
+
const limit = Math.min(Math.max(Math.floor(input.limit ?? 50), 1), 200);
|
|
42709
|
+
const display = candidates.slice(0, limit);
|
|
42710
|
+
const omittedCount = Math.max(0, candidates.length - display.length);
|
|
42711
|
+
if (!input.apply) {
|
|
42712
|
+
return {
|
|
42713
|
+
schema_version: TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
|
|
42714
|
+
local_only: true,
|
|
42715
|
+
dry_run: true,
|
|
42716
|
+
processed_at: timestamp2,
|
|
42717
|
+
action: candidates.length > 0 ? "preview" : "noop",
|
|
42718
|
+
task_id: input.task_id,
|
|
42719
|
+
source: input.source ? redactOptional(input.source, 120) : null,
|
|
42720
|
+
run_id: runId,
|
|
42721
|
+
present_fingerprint_count: present.size,
|
|
42722
|
+
candidate_count: candidates.length,
|
|
42723
|
+
changed_count: 0,
|
|
42724
|
+
omitted_count: omittedCount,
|
|
42725
|
+
findings: display.map(compactFinding),
|
|
42726
|
+
warnings
|
|
42727
|
+
};
|
|
42728
|
+
}
|
|
42729
|
+
const metadataPatch = redactValue({
|
|
42730
|
+
resolved_by: {
|
|
42731
|
+
agent_id: input.agent_id ?? null,
|
|
42732
|
+
run_id: runId,
|
|
42733
|
+
reason: input.reason ? redactEvidenceText(input.reason) : "missing from latest loop finding set"
|
|
42734
|
+
}
|
|
42735
|
+
});
|
|
42736
|
+
const tx = d.transaction(() => {
|
|
42737
|
+
for (const finding2 of candidates) {
|
|
42738
|
+
d.run("UPDATE task_findings SET status = ?, resolved_at = ?, metadata = ?, updated_at = ? WHERE id = ? AND status = 'open'", [
|
|
42739
|
+
status,
|
|
42740
|
+
timestamp2,
|
|
42741
|
+
JSON.stringify({ ...finding2.metadata, ...metadataPatch }),
|
|
42742
|
+
timestamp2,
|
|
42743
|
+
finding2.id
|
|
42744
|
+
]);
|
|
42745
|
+
}
|
|
42746
|
+
});
|
|
42747
|
+
tx();
|
|
42748
|
+
const updated = candidates.map((finding2) => d.query("SELECT * FROM task_findings WHERE id = ?").get(finding2.id)).filter((row) => Boolean(row)).map(rowToFinding);
|
|
42749
|
+
const visibleUpdated = updated.slice(0, limit);
|
|
42750
|
+
return {
|
|
42751
|
+
schema_version: TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
|
|
42752
|
+
local_only: true,
|
|
42753
|
+
dry_run: false,
|
|
42754
|
+
processed_at: timestamp2,
|
|
42755
|
+
action: updated.length > 0 ? status : "noop",
|
|
42756
|
+
task_id: input.task_id,
|
|
42757
|
+
source: input.source ? redactOptional(input.source, 120) : null,
|
|
42758
|
+
run_id: runId,
|
|
42759
|
+
present_fingerprint_count: present.size,
|
|
42760
|
+
candidate_count: candidates.length,
|
|
42761
|
+
changed_count: updated.length,
|
|
42762
|
+
omitted_count: omittedCount,
|
|
42763
|
+
findings: visibleUpdated.map(compactFinding),
|
|
42764
|
+
warnings
|
|
42765
|
+
};
|
|
42766
|
+
}
|
|
41828
42767
|
|
|
41829
42768
|
// src/index.ts
|
|
41830
42769
|
init_redaction();
|
|
@@ -43155,6 +44094,8 @@ export {
|
|
|
43155
44094
|
upsertTesterIssueReports,
|
|
43156
44095
|
upsertTesterIssueReport,
|
|
43157
44096
|
upsertTerminalNotificationRule,
|
|
44097
|
+
upsertTaskFinding,
|
|
44098
|
+
upsertTaskByFingerprint,
|
|
43158
44099
|
upsertSecretSafetyConfig,
|
|
43159
44100
|
upsertRunnerSandboxProfile,
|
|
43160
44101
|
upsertReviewRoutingRule,
|
|
@@ -43204,6 +44145,7 @@ export {
|
|
|
43204
44145
|
syncWithAgent,
|
|
43205
44146
|
syncKgEdges,
|
|
43206
44147
|
supersedeDecisionRecord,
|
|
44148
|
+
summarizeTaskRun,
|
|
43207
44149
|
summarizeRoadmap,
|
|
43208
44150
|
summarizeMilestone,
|
|
43209
44151
|
suggestAgentNames,
|
|
@@ -43281,6 +44223,7 @@ export {
|
|
|
43281
44223
|
resolvePlanRef,
|
|
43282
44224
|
resolvePlanId,
|
|
43283
44225
|
resolvePartialId,
|
|
44226
|
+
resolveMissingTaskFindings,
|
|
43284
44227
|
resolveMentions,
|
|
43285
44228
|
resolveGitRoot,
|
|
43286
44229
|
resolveCommandQuery,
|
|
@@ -43455,6 +44398,7 @@ export {
|
|
|
43455
44398
|
listTasks,
|
|
43456
44399
|
listTaskRuns,
|
|
43457
44400
|
listTaskLists,
|
|
44401
|
+
listTaskFindings,
|
|
43458
44402
|
listTaskFiles,
|
|
43459
44403
|
listTaskBoards,
|
|
43460
44404
|
listSubscriptions,
|
|
@@ -43507,6 +44451,7 @@ export {
|
|
|
43507
44451
|
listCyclesWithStats,
|
|
43508
44452
|
listCycles,
|
|
43509
44453
|
listCustomFieldDefinitions,
|
|
44454
|
+
listCompactTaskFindings,
|
|
43510
44455
|
listComments,
|
|
43511
44456
|
listCommandAliases,
|
|
43512
44457
|
listCapacityProfiles,
|
|
@@ -43613,6 +44558,7 @@ export {
|
|
|
43613
44558
|
getTaskCustomFields,
|
|
43614
44559
|
getTaskContract,
|
|
43615
44560
|
getTaskCommits,
|
|
44561
|
+
getTaskByFingerprint,
|
|
43616
44562
|
getTaskBoard,
|
|
43617
44563
|
getTask,
|
|
43618
44564
|
getStoredHandoffAsPacket,
|
|
@@ -43792,10 +44738,12 @@ export {
|
|
|
43792
44738
|
formatDecisionRecordMarkdown,
|
|
43793
44739
|
formatAgentWorkflowDemoReport,
|
|
43794
44740
|
formatActivityRecordText,
|
|
44741
|
+
finishTaskRunTransaction,
|
|
43795
44742
|
finishTaskRun,
|
|
43796
44743
|
fingerprintTesterIssueReport,
|
|
43797
44744
|
fingerprintInboxInput,
|
|
43798
44745
|
findTasksByFile,
|
|
44746
|
+
findTaskRunByTransactionKey,
|
|
43799
44747
|
findTaskByCommit,
|
|
43800
44748
|
findRelatedTaskIds,
|
|
43801
44749
|
findPath,
|
|
@@ -44017,6 +44965,7 @@ export {
|
|
|
44017
44965
|
buildCodebaseIndex,
|
|
44018
44966
|
buildArtifactExportManifest,
|
|
44019
44967
|
bootstrapProject,
|
|
44968
|
+
beginTaskRunTransaction,
|
|
44020
44969
|
backupDatabase,
|
|
44021
44970
|
autoReleaseStaleAgents,
|
|
44022
44971
|
autoDetectFileRelationships,
|
|
@@ -44102,6 +45051,9 @@ export {
|
|
|
44102
45051
|
TASK_STATUSES,
|
|
44103
45052
|
TASK_SCHEDULING_SCHEMA,
|
|
44104
45053
|
TASK_PRIORITIES,
|
|
45054
|
+
TASK_FINDING_UPSERT_SCHEMA_VERSION,
|
|
45055
|
+
TASK_FINDING_SCHEMA_VERSION,
|
|
45056
|
+
TASK_FINDING_RESOLVE_MISSING_SCHEMA_VERSION,
|
|
44105
45057
|
STORAGE_TABLES,
|
|
44106
45058
|
SECRET_REDACTION_SCHEMA,
|
|
44107
45059
|
SCHEMA_SEMVER,
|
|
@@ -44138,6 +45090,7 @@ export {
|
|
|
44138
45090
|
MANPAGE_SCHEMA,
|
|
44139
45091
|
MACHINE_TOPOLOGY_SCHEMA,
|
|
44140
45092
|
LockError,
|
|
45093
|
+
LOOP_RUN_TRANSACTION_SCHEMA_VERSION,
|
|
44141
45094
|
LOCAL_USAGE_LEDGER_SCHEMA_VERSION,
|
|
44142
45095
|
LOCAL_ROADMAP_SCHEMA_VERSION,
|
|
44143
45096
|
LOCAL_REPORT_TYPES,
|