@hasna/todos 0.11.43 → 0.11.45
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 +573 -11
- package/dist/cli/commands/agent-reliability-commands.d.ts +3 -0
- package/dist/cli/commands/agent-reliability-commands.d.ts.map +1 -0
- package/dist/cli/commands/audit-ledger-commands.d.ts +3 -0
- package/dist/cli/commands/audit-ledger-commands.d.ts.map +1 -0
- package/dist/cli/commands/capacity-commands.d.ts +3 -0
- package/dist/cli/commands/capacity-commands.d.ts.map +1 -0
- package/dist/cli/commands/config-serve-commands.d.ts.map +1 -1
- package/dist/cli/commands/environment-snapshots.d.ts +3 -0
- package/dist/cli/commands/environment-snapshots.d.ts.map +1 -0
- package/dist/cli/commands/knowledge-commands.d.ts +3 -0
- package/dist/cli/commands/knowledge-commands.d.ts.map +1 -0
- package/dist/cli/commands/local-snapshot-commands.d.ts +3 -0
- package/dist/cli/commands/local-snapshot-commands.d.ts.map +1 -0
- package/dist/cli/commands/machines.d.ts.map +1 -1
- package/dist/cli/commands/mcp-hooks-commands.d.ts.map +1 -1
- package/dist/cli/commands/onboarding-commands.d.ts +3 -0
- package/dist/cli/commands/onboarding-commands.d.ts.map +1 -0
- package/dist/cli/commands/project-commands.d.ts.map +1 -1
- package/dist/cli/commands/query-commands.d.ts.map +1 -1
- package/dist/cli/commands/release-compatibility-commands.d.ts +3 -0
- package/dist/cli/commands/release-compatibility-commands.d.ts.map +1 -0
- package/dist/cli/commands/retrospective-commands.d.ts +3 -0
- package/dist/cli/commands/retrospective-commands.d.ts.map +1 -0
- package/dist/cli/commands/review-queue-commands.d.ts +3 -0
- package/dist/cli/commands/review-queue-commands.d.ts.map +1 -0
- package/dist/cli/commands/risk-commands.d.ts +3 -0
- package/dist/cli/commands/risk-commands.d.ts.map +1 -0
- package/dist/cli/commands/roadmap-commands.d.ts +3 -0
- package/dist/cli/commands/roadmap-commands.d.ts.map +1 -0
- package/dist/cli/commands/sdk-fixture-commands.d.ts +3 -0
- package/dist/cli/commands/sdk-fixture-commands.d.ts.map +1 -0
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/index.js +26227 -9412
- package/dist/cli-mcp-parity.d.ts +1 -1
- package/dist/cli-mcp-parity.d.ts.map +1 -1
- package/dist/contracts.d.ts +19 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +10174 -679
- package/dist/db/agent-metrics.d.ts +101 -0
- package/dist/db/agent-metrics.d.ts.map +1 -1
- package/dist/db/boards.d.ts +56 -0
- package/dist/db/boards.d.ts.map +1 -0
- package/dist/db/calendar.d.ts +52 -0
- package/dist/db/calendar.d.ts.map +1 -0
- package/dist/db/comments.d.ts.map +1 -1
- package/dist/db/handoffs.d.ts +25 -0
- package/dist/db/handoffs.d.ts.map +1 -1
- package/dist/db/machines.d.ts +19 -6
- package/dist/db/machines.d.ts.map +1 -1
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/project-knowledge.d.ts +88 -0
- package/dist/db/project-knowledge.d.ts.map +1 -0
- package/dist/db/project-risks.d.ts +139 -0
- package/dist/db/project-risks.d.ts.map +1 -0
- package/dist/db/retrospectives.d.ts +98 -0
- package/dist/db/retrospectives.d.ts.map +1 -0
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/task-crud.d.ts.map +1 -1
- package/dist/db/task-relations.d.ts +69 -9
- package/dist/db/task-relations.d.ts.map +1 -1
- package/dist/db/tasks.d.ts +6 -2
- package/dist/db/tasks.d.ts.map +1 -1
- package/dist/index.d.ts +62 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19485 -8856
- package/dist/json-contracts.d.ts.map +1 -1
- package/dist/lib/agent-replay-simulator.d.ts +66 -0
- package/dist/lib/agent-replay-simulator.d.ts.map +1 -0
- package/dist/lib/audit-ledger.d.ts +59 -0
- package/dist/lib/audit-ledger.d.ts.map +1 -0
- package/dist/lib/branch-work-plans.d.ts +46 -0
- package/dist/lib/branch-work-plans.d.ts.map +1 -0
- package/dist/lib/capacity-forecasts.d.ts +70 -0
- package/dist/lib/capacity-forecasts.d.ts.map +1 -0
- package/dist/lib/config.d.ts +179 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/context-packs.d.ts +26 -3
- package/dist/lib/context-packs.d.ts.map +1 -1
- package/dist/lib/environment-snapshots.d.ts +111 -0
- package/dist/lib/environment-snapshots.d.ts.map +1 -0
- package/dist/lib/event-hooks.d.ts +1 -1
- package/dist/lib/event-hooks.d.ts.map +1 -1
- package/dist/lib/external-issue-importers.d.ts +60 -0
- package/dist/lib/external-issue-importers.d.ts.map +1 -0
- package/dist/lib/extract.d.ts +57 -0
- package/dist/lib/extract.d.ts.map +1 -1
- package/dist/lib/local-bridge.d.ts +3 -1
- package/dist/lib/local-bridge.d.ts.map +1 -1
- package/dist/lib/local-extensions.d.ts +75 -0
- package/dist/lib/local-extensions.d.ts.map +1 -0
- package/dist/lib/local-notifications.d.ts +55 -0
- package/dist/lib/local-notifications.d.ts.map +1 -0
- package/dist/lib/local-snapshots.d.ts +66 -0
- package/dist/lib/local-snapshots.d.ts.map +1 -0
- package/dist/lib/mention-resolver.d.ts +43 -0
- package/dist/lib/mention-resolver.d.ts.map +1 -0
- package/dist/lib/natural-language-intake.d.ts +56 -0
- package/dist/lib/natural-language-intake.d.ts.map +1 -0
- package/dist/lib/onboarding-fixtures.d.ts +31 -0
- package/dist/lib/onboarding-fixtures.d.ts.map +1 -0
- package/dist/lib/public-release-gate.d.ts +7 -0
- package/dist/lib/public-release-gate.d.ts.map +1 -1
- package/dist/lib/redaction.d.ts +9 -0
- package/dist/lib/redaction.d.ts.map +1 -1
- package/dist/lib/release-compatibility.d.ts +59 -0
- package/dist/lib/release-compatibility.d.ts.map +1 -0
- package/dist/lib/release-notes.d.ts +81 -0
- package/dist/lib/release-notes.d.ts.map +1 -0
- package/dist/lib/retention-cleanup.d.ts +63 -0
- package/dist/lib/retention-cleanup.d.ts.map +1 -0
- package/dist/lib/review-queues.d.ts +98 -0
- package/dist/lib/review-queues.d.ts.map +1 -0
- package/dist/lib/roadmaps.d.ts +133 -0
- package/dist/lib/roadmaps.d.ts.map +1 -0
- package/dist/lib/sdk-integration-fixtures.d.ts +65 -0
- package/dist/lib/sdk-integration-fixtures.d.ts.map +1 -0
- package/dist/lib/terminal-notifications.d.ts +53 -0
- package/dist/lib/terminal-notifications.d.ts.map +1 -0
- package/dist/lib/todos-md.d.ts.map +1 -1
- package/dist/lib/workflow-prompts.d.ts +38 -0
- package/dist/lib/workflow-prompts.d.ts.map +1 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +20925 -9542
- package/dist/mcp/token-utils.d.ts.map +1 -1
- package/dist/mcp/tools/code-tools.d.ts.map +1 -1
- package/dist/mcp/tools/environment-snapshots.d.ts +8 -0
- package/dist/mcp/tools/environment-snapshots.d.ts.map +1 -0
- package/dist/mcp/tools/machines.d.ts.map +1 -1
- package/dist/mcp/tools/task-adv-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-auto-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-crud.d.ts.map +1 -1
- package/dist/mcp/tools/task-meta-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-rel-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-resources.d.ts.map +1 -1
- package/dist/mcp/tools/workflow-prompts.d.ts +3 -0
- package/dist/mcp/tools/workflow-prompts.d.ts.map +1 -0
- package/dist/mcp.js +97 -2
- package/dist/registry.js +14462 -5998
- package/dist/release-provenance.json +3 -3
- package/dist/server/index.js +493 -123
- package/dist/storage.js +2353 -139
- package/dist/types/index.d.ts +214 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/storage.js
CHANGED
|
@@ -1122,6 +1122,152 @@ var init_migrations = __esm(() => {
|
|
|
1122
1122
|
);
|
|
1123
1123
|
CREATE INDEX IF NOT EXISTS idx_saved_search_views_scope ON saved_search_views(scope);
|
|
1124
1124
|
INSERT OR IGNORE INTO _migrations (id) VALUES (55);
|
|
1125
|
+
`,
|
|
1126
|
+
`
|
|
1127
|
+
ALTER TABLE task_time_logs ADD COLUMN run_id TEXT;
|
|
1128
|
+
ALTER TABLE task_time_logs ADD COLUMN focus_session_id TEXT;
|
|
1129
|
+
CREATE INDEX IF NOT EXISTS idx_task_time_logs_run ON task_time_logs(run_id);
|
|
1130
|
+
CREATE INDEX IF NOT EXISTS idx_task_time_logs_focus_session ON task_time_logs(focus_session_id);
|
|
1131
|
+
CREATE TABLE IF NOT EXISTS focus_sessions (
|
|
1132
|
+
id TEXT PRIMARY KEY,
|
|
1133
|
+
task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
|
|
1134
|
+
plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
|
|
1135
|
+
run_id TEXT,
|
|
1136
|
+
agent_id TEXT,
|
|
1137
|
+
title TEXT,
|
|
1138
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'paused', 'completed', 'cancelled')),
|
|
1139
|
+
started_at TEXT NOT NULL,
|
|
1140
|
+
last_resumed_at TEXT,
|
|
1141
|
+
paused_at TEXT,
|
|
1142
|
+
ended_at TEXT,
|
|
1143
|
+
actual_minutes INTEGER NOT NULL DEFAULT 0,
|
|
1144
|
+
idle_after_minutes INTEGER,
|
|
1145
|
+
notes TEXT,
|
|
1146
|
+
metadata TEXT DEFAULT '{}',
|
|
1147
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1148
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1149
|
+
);
|
|
1150
|
+
CREATE INDEX IF NOT EXISTS idx_focus_sessions_task ON focus_sessions(task_id);
|
|
1151
|
+
CREATE INDEX IF NOT EXISTS idx_focus_sessions_plan ON focus_sessions(plan_id);
|
|
1152
|
+
CREATE INDEX IF NOT EXISTS idx_focus_sessions_run ON focus_sessions(run_id);
|
|
1153
|
+
CREATE INDEX IF NOT EXISTS idx_focus_sessions_agent ON focus_sessions(agent_id);
|
|
1154
|
+
CREATE INDEX IF NOT EXISTS idx_focus_sessions_status ON focus_sessions(status);
|
|
1155
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (56);
|
|
1156
|
+
`,
|
|
1157
|
+
`
|
|
1158
|
+
CREATE TABLE IF NOT EXISTS task_boards (
|
|
1159
|
+
id TEXT PRIMARY KEY,
|
|
1160
|
+
name TEXT NOT NULL UNIQUE,
|
|
1161
|
+
scope TEXT NOT NULL DEFAULT 'tasks' CHECK(scope IN ('tasks', 'plans')),
|
|
1162
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
1163
|
+
task_list_id TEXT REFERENCES task_lists(id) ON DELETE SET NULL,
|
|
1164
|
+
plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
|
|
1165
|
+
agent_id TEXT,
|
|
1166
|
+
lanes TEXT NOT NULL DEFAULT '[]',
|
|
1167
|
+
filters TEXT NOT NULL DEFAULT '{}',
|
|
1168
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1169
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1170
|
+
);
|
|
1171
|
+
CREATE INDEX IF NOT EXISTS idx_task_boards_scope ON task_boards(scope);
|
|
1172
|
+
CREATE INDEX IF NOT EXISTS idx_task_boards_project ON task_boards(project_id);
|
|
1173
|
+
CREATE INDEX IF NOT EXISTS idx_task_boards_plan ON task_boards(plan_id);
|
|
1174
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (57);
|
|
1175
|
+
`,
|
|
1176
|
+
`
|
|
1177
|
+
CREATE TABLE IF NOT EXISTS local_calendar_items (
|
|
1178
|
+
id TEXT PRIMARY KEY,
|
|
1179
|
+
kind TEXT NOT NULL CHECK(kind IN ('task_due', 'task_sla', 'task_reminder', 'milestone', 'work_block', 'run', 'imported')),
|
|
1180
|
+
title TEXT NOT NULL,
|
|
1181
|
+
description TEXT,
|
|
1182
|
+
starts_at TEXT NOT NULL,
|
|
1183
|
+
ends_at TEXT,
|
|
1184
|
+
timezone TEXT,
|
|
1185
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
1186
|
+
task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
|
|
1187
|
+
plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
|
|
1188
|
+
run_id TEXT,
|
|
1189
|
+
recurrence_rule TEXT,
|
|
1190
|
+
metadata TEXT DEFAULT '{}',
|
|
1191
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1192
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1193
|
+
);
|
|
1194
|
+
CREATE INDEX IF NOT EXISTS idx_local_calendar_items_time ON local_calendar_items(starts_at, ends_at);
|
|
1195
|
+
CREATE INDEX IF NOT EXISTS idx_local_calendar_items_task ON local_calendar_items(task_id);
|
|
1196
|
+
CREATE INDEX IF NOT EXISTS idx_local_calendar_items_project ON local_calendar_items(project_id);
|
|
1197
|
+
CREATE INDEX IF NOT EXISTS idx_local_calendar_items_kind ON local_calendar_items(kind);
|
|
1198
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (58);
|
|
1199
|
+
`,
|
|
1200
|
+
`
|
|
1201
|
+
CREATE TABLE IF NOT EXISTS project_knowledge_records (
|
|
1202
|
+
id TEXT PRIMARY KEY,
|
|
1203
|
+
record_type TEXT NOT NULL CHECK(record_type IN ('decision','architecture_note','tradeoff','context_snapshot')),
|
|
1204
|
+
title TEXT NOT NULL,
|
|
1205
|
+
content TEXT,
|
|
1206
|
+
decision TEXT,
|
|
1207
|
+
rationale TEXT,
|
|
1208
|
+
alternatives TEXT DEFAULT '[]',
|
|
1209
|
+
task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
|
|
1210
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
1211
|
+
plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
|
|
1212
|
+
agent_id TEXT,
|
|
1213
|
+
snapshot_id TEXT REFERENCES context_snapshots(id) ON DELETE SET NULL,
|
|
1214
|
+
tags TEXT DEFAULT '[]',
|
|
1215
|
+
metadata TEXT DEFAULT '{}',
|
|
1216
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1217
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1218
|
+
);
|
|
1219
|
+
CREATE INDEX IF NOT EXISTS idx_project_knowledge_type ON project_knowledge_records(record_type);
|
|
1220
|
+
CREATE INDEX IF NOT EXISTS idx_project_knowledge_project ON project_knowledge_records(project_id);
|
|
1221
|
+
CREATE INDEX IF NOT EXISTS idx_project_knowledge_task ON project_knowledge_records(task_id);
|
|
1222
|
+
CREATE INDEX IF NOT EXISTS idx_project_knowledge_plan ON project_knowledge_records(plan_id);
|
|
1223
|
+
CREATE INDEX IF NOT EXISTS idx_project_knowledge_agent ON project_knowledge_records(agent_id);
|
|
1224
|
+
CREATE INDEX IF NOT EXISTS idx_project_knowledge_snapshot ON project_knowledge_records(snapshot_id);
|
|
1225
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (59);
|
|
1226
|
+
`,
|
|
1227
|
+
`
|
|
1228
|
+
CREATE TABLE IF NOT EXISTS project_risks (
|
|
1229
|
+
id TEXT PRIMARY KEY,
|
|
1230
|
+
title TEXT NOT NULL,
|
|
1231
|
+
description TEXT,
|
|
1232
|
+
status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open','mitigating','resolved','accepted')),
|
|
1233
|
+
severity TEXT NOT NULL DEFAULT 'medium' CHECK(severity IN ('low','medium','high','critical')),
|
|
1234
|
+
probability TEXT NOT NULL DEFAULT 'medium' CHECK(probability IN ('low','medium','high')),
|
|
1235
|
+
owner TEXT,
|
|
1236
|
+
mitigation TEXT,
|
|
1237
|
+
due_at TEXT,
|
|
1238
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
1239
|
+
plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
|
|
1240
|
+
task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
|
|
1241
|
+
tags TEXT DEFAULT '[]',
|
|
1242
|
+
metadata TEXT DEFAULT '{}',
|
|
1243
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1244
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1245
|
+
closed_at TEXT
|
|
1246
|
+
);
|
|
1247
|
+
CREATE INDEX IF NOT EXISTS idx_project_risks_status ON project_risks(status);
|
|
1248
|
+
CREATE INDEX IF NOT EXISTS idx_project_risks_severity ON project_risks(severity);
|
|
1249
|
+
CREATE INDEX IF NOT EXISTS idx_project_risks_project ON project_risks(project_id);
|
|
1250
|
+
CREATE INDEX IF NOT EXISTS idx_project_risks_plan ON project_risks(plan_id);
|
|
1251
|
+
CREATE INDEX IF NOT EXISTS idx_project_risks_task ON project_risks(task_id);
|
|
1252
|
+
CREATE INDEX IF NOT EXISTS idx_project_risks_due ON project_risks(due_at);
|
|
1253
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (60);
|
|
1254
|
+
`,
|
|
1255
|
+
`
|
|
1256
|
+
CREATE TABLE IF NOT EXISTS local_retrospectives (
|
|
1257
|
+
id TEXT PRIMARY KEY,
|
|
1258
|
+
title TEXT NOT NULL,
|
|
1259
|
+
scope TEXT NOT NULL CHECK(scope IN ('project','plan')),
|
|
1260
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
1261
|
+
plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
|
|
1262
|
+
agent_id TEXT,
|
|
1263
|
+
report_json TEXT NOT NULL,
|
|
1264
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1265
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1266
|
+
);
|
|
1267
|
+
CREATE INDEX IF NOT EXISTS idx_local_retrospectives_project ON local_retrospectives(project_id);
|
|
1268
|
+
CREATE INDEX IF NOT EXISTS idx_local_retrospectives_plan ON local_retrospectives(plan_id);
|
|
1269
|
+
CREATE INDEX IF NOT EXISTS idx_local_retrospectives_agent ON local_retrospectives(agent_id);
|
|
1270
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (61);
|
|
1125
1271
|
`
|
|
1126
1272
|
];
|
|
1127
1273
|
});
|
|
@@ -1309,6 +1455,89 @@ function ensureSchema(db) {
|
|
|
1309
1455
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1310
1456
|
)`);
|
|
1311
1457
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_saved_search_views_scope ON saved_search_views(scope)");
|
|
1458
|
+
ensureTable("context_snapshots", `
|
|
1459
|
+
CREATE TABLE context_snapshots (
|
|
1460
|
+
id TEXT PRIMARY KEY,
|
|
1461
|
+
agent_id TEXT,
|
|
1462
|
+
task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
|
|
1463
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
1464
|
+
snapshot_type TEXT NOT NULL CHECK(snapshot_type IN ('interrupt','complete','handoff','checkpoint')),
|
|
1465
|
+
plan_summary TEXT,
|
|
1466
|
+
files_open TEXT DEFAULT '[]',
|
|
1467
|
+
attempts TEXT DEFAULT '[]',
|
|
1468
|
+
blockers TEXT DEFAULT '[]',
|
|
1469
|
+
next_steps TEXT,
|
|
1470
|
+
metadata TEXT DEFAULT '{}',
|
|
1471
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1472
|
+
)`);
|
|
1473
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_snapshots_agent ON context_snapshots(agent_id)");
|
|
1474
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_snapshots_task ON context_snapshots(task_id)");
|
|
1475
|
+
ensureTable("project_knowledge_records", `
|
|
1476
|
+
CREATE TABLE project_knowledge_records (
|
|
1477
|
+
id TEXT PRIMARY KEY,
|
|
1478
|
+
record_type TEXT NOT NULL CHECK(record_type IN ('decision','architecture_note','tradeoff','context_snapshot')),
|
|
1479
|
+
title TEXT NOT NULL,
|
|
1480
|
+
content TEXT,
|
|
1481
|
+
decision TEXT,
|
|
1482
|
+
rationale TEXT,
|
|
1483
|
+
alternatives TEXT DEFAULT '[]',
|
|
1484
|
+
task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
|
|
1485
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
1486
|
+
plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
|
|
1487
|
+
agent_id TEXT,
|
|
1488
|
+
snapshot_id TEXT REFERENCES context_snapshots(id) ON DELETE SET NULL,
|
|
1489
|
+
tags TEXT DEFAULT '[]',
|
|
1490
|
+
metadata TEXT DEFAULT '{}',
|
|
1491
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1492
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1493
|
+
)`);
|
|
1494
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_knowledge_type ON project_knowledge_records(record_type)");
|
|
1495
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_knowledge_project ON project_knowledge_records(project_id)");
|
|
1496
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_knowledge_task ON project_knowledge_records(task_id)");
|
|
1497
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_knowledge_plan ON project_knowledge_records(plan_id)");
|
|
1498
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_knowledge_agent ON project_knowledge_records(agent_id)");
|
|
1499
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_knowledge_snapshot ON project_knowledge_records(snapshot_id)");
|
|
1500
|
+
ensureTable("project_risks", `
|
|
1501
|
+
CREATE TABLE project_risks (
|
|
1502
|
+
id TEXT PRIMARY KEY,
|
|
1503
|
+
title TEXT NOT NULL,
|
|
1504
|
+
description TEXT,
|
|
1505
|
+
status TEXT NOT NULL DEFAULT 'open' CHECK(status IN ('open','mitigating','resolved','accepted')),
|
|
1506
|
+
severity TEXT NOT NULL DEFAULT 'medium' CHECK(severity IN ('low','medium','high','critical')),
|
|
1507
|
+
probability TEXT NOT NULL DEFAULT 'medium' CHECK(probability IN ('low','medium','high')),
|
|
1508
|
+
owner TEXT,
|
|
1509
|
+
mitigation TEXT,
|
|
1510
|
+
due_at TEXT,
|
|
1511
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
1512
|
+
plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
|
|
1513
|
+
task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
|
|
1514
|
+
tags TEXT DEFAULT '[]',
|
|
1515
|
+
metadata TEXT DEFAULT '{}',
|
|
1516
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1517
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1518
|
+
closed_at TEXT
|
|
1519
|
+
)`);
|
|
1520
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_risks_status ON project_risks(status)");
|
|
1521
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_risks_severity ON project_risks(severity)");
|
|
1522
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_risks_project ON project_risks(project_id)");
|
|
1523
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_risks_plan ON project_risks(plan_id)");
|
|
1524
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_risks_task ON project_risks(task_id)");
|
|
1525
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_project_risks_due ON project_risks(due_at)");
|
|
1526
|
+
ensureTable("local_retrospectives", `
|
|
1527
|
+
CREATE TABLE local_retrospectives (
|
|
1528
|
+
id TEXT PRIMARY KEY,
|
|
1529
|
+
title TEXT NOT NULL,
|
|
1530
|
+
scope TEXT NOT NULL CHECK(scope IN ('project','plan')),
|
|
1531
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
1532
|
+
plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
|
|
1533
|
+
agent_id TEXT,
|
|
1534
|
+
report_json TEXT NOT NULL,
|
|
1535
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1536
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1537
|
+
)`);
|
|
1538
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_local_retrospectives_project ON local_retrospectives(project_id)");
|
|
1539
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_local_retrospectives_plan ON local_retrospectives(plan_id)");
|
|
1540
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_local_retrospectives_agent ON local_retrospectives(agent_id)");
|
|
1312
1541
|
ensureTable("task_relationships", `
|
|
1313
1542
|
CREATE TABLE task_relationships (
|
|
1314
1543
|
id TEXT PRIMARY KEY,
|
|
@@ -1645,6 +1874,8 @@ function ensureSchema(db) {
|
|
|
1645
1874
|
CREATE TABLE task_time_logs (
|
|
1646
1875
|
id TEXT PRIMARY KEY,
|
|
1647
1876
|
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1877
|
+
run_id TEXT,
|
|
1878
|
+
focus_session_id TEXT,
|
|
1648
1879
|
agent_id TEXT,
|
|
1649
1880
|
started_at TEXT,
|
|
1650
1881
|
ended_at TEXT,
|
|
@@ -1652,9 +1883,77 @@ function ensureSchema(db) {
|
|
|
1652
1883
|
notes TEXT,
|
|
1653
1884
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1654
1885
|
)`);
|
|
1886
|
+
ensureColumn("task_time_logs", "run_id", "TEXT");
|
|
1887
|
+
ensureColumn("task_time_logs", "focus_session_id", "TEXT");
|
|
1655
1888
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_time_logs_task ON task_time_logs(task_id)");
|
|
1656
1889
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_time_logs_agent ON task_time_logs(agent_id)");
|
|
1890
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_time_logs_run ON task_time_logs(run_id)");
|
|
1891
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_time_logs_focus_session ON task_time_logs(focus_session_id)");
|
|
1657
1892
|
ensureColumn("tasks", "actual_minutes", "INTEGER");
|
|
1893
|
+
ensureTable("focus_sessions", `
|
|
1894
|
+
CREATE TABLE focus_sessions (
|
|
1895
|
+
id TEXT PRIMARY KEY,
|
|
1896
|
+
task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
|
|
1897
|
+
plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
|
|
1898
|
+
run_id TEXT,
|
|
1899
|
+
agent_id TEXT,
|
|
1900
|
+
title TEXT,
|
|
1901
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'paused', 'completed', 'cancelled')),
|
|
1902
|
+
started_at TEXT NOT NULL,
|
|
1903
|
+
last_resumed_at TEXT,
|
|
1904
|
+
paused_at TEXT,
|
|
1905
|
+
ended_at TEXT,
|
|
1906
|
+
actual_minutes INTEGER NOT NULL DEFAULT 0,
|
|
1907
|
+
idle_after_minutes INTEGER,
|
|
1908
|
+
notes TEXT,
|
|
1909
|
+
metadata TEXT DEFAULT '{}',
|
|
1910
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1911
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1912
|
+
)`);
|
|
1913
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_focus_sessions_task ON focus_sessions(task_id)");
|
|
1914
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_focus_sessions_plan ON focus_sessions(plan_id)");
|
|
1915
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_focus_sessions_run ON focus_sessions(run_id)");
|
|
1916
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_focus_sessions_agent ON focus_sessions(agent_id)");
|
|
1917
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_focus_sessions_status ON focus_sessions(status)");
|
|
1918
|
+
ensureTable("task_boards", `
|
|
1919
|
+
CREATE TABLE task_boards (
|
|
1920
|
+
id TEXT PRIMARY KEY,
|
|
1921
|
+
name TEXT NOT NULL UNIQUE,
|
|
1922
|
+
scope TEXT NOT NULL DEFAULT 'tasks' CHECK(scope IN ('tasks', 'plans')),
|
|
1923
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
1924
|
+
task_list_id TEXT REFERENCES task_lists(id) ON DELETE SET NULL,
|
|
1925
|
+
plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
|
|
1926
|
+
agent_id TEXT,
|
|
1927
|
+
lanes TEXT NOT NULL DEFAULT '[]',
|
|
1928
|
+
filters TEXT NOT NULL DEFAULT '{}',
|
|
1929
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1930
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1931
|
+
)`);
|
|
1932
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_boards_scope ON task_boards(scope)");
|
|
1933
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_boards_project ON task_boards(project_id)");
|
|
1934
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_boards_plan ON task_boards(plan_id)");
|
|
1935
|
+
ensureTable("local_calendar_items", `
|
|
1936
|
+
CREATE TABLE local_calendar_items (
|
|
1937
|
+
id TEXT PRIMARY KEY,
|
|
1938
|
+
kind TEXT NOT NULL CHECK(kind IN ('task_due', 'task_sla', 'task_reminder', 'milestone', 'work_block', 'run', 'imported')),
|
|
1939
|
+
title TEXT NOT NULL,
|
|
1940
|
+
description TEXT,
|
|
1941
|
+
starts_at TEXT NOT NULL,
|
|
1942
|
+
ends_at TEXT,
|
|
1943
|
+
timezone TEXT,
|
|
1944
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
1945
|
+
task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
|
|
1946
|
+
plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
|
|
1947
|
+
run_id TEXT,
|
|
1948
|
+
recurrence_rule TEXT,
|
|
1949
|
+
metadata TEXT DEFAULT '{}',
|
|
1950
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1951
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1952
|
+
)`);
|
|
1953
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_local_calendar_items_time ON local_calendar_items(starts_at, ends_at)");
|
|
1954
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_local_calendar_items_task ON local_calendar_items(task_id)");
|
|
1955
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_local_calendar_items_project ON local_calendar_items(project_id)");
|
|
1956
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_local_calendar_items_kind ON local_calendar_items(kind)");
|
|
1658
1957
|
ensureTable("task_watchers", `
|
|
1659
1958
|
CREATE TABLE task_watchers (
|
|
1660
1959
|
id TEXT PRIMARY KEY,
|
|
@@ -1734,12 +2033,63 @@ var init_schema = __esm(() => {
|
|
|
1734
2033
|
});
|
|
1735
2034
|
|
|
1736
2035
|
// src/db/machines.ts
|
|
1737
|
-
import {
|
|
2036
|
+
import { existsSync } from "fs";
|
|
2037
|
+
import { hostname as osHostname, platform as osPlatform, arch as osArch } from "os";
|
|
2038
|
+
import { resolve } from "path";
|
|
2039
|
+
import { spawnSync } from "child_process";
|
|
2040
|
+
function parseMetadata(value) {
|
|
2041
|
+
if (!value)
|
|
2042
|
+
return {};
|
|
2043
|
+
try {
|
|
2044
|
+
const parsed = JSON.parse(value);
|
|
2045
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
2046
|
+
} catch {
|
|
2047
|
+
return {};
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
1738
2050
|
function rowToMachine(row) {
|
|
1739
2051
|
return {
|
|
1740
2052
|
...row,
|
|
1741
2053
|
is_primary: !!row.is_primary,
|
|
1742
|
-
metadata:
|
|
2054
|
+
metadata: parseMetadata(row.metadata)
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
function discoverGitRoot(workspacePath) {
|
|
2058
|
+
const result = spawnSync("git", ["-C", workspacePath, "rev-parse", "--show-toplevel"], {
|
|
2059
|
+
encoding: "utf-8",
|
|
2060
|
+
timeout: 2000
|
|
2061
|
+
});
|
|
2062
|
+
if (result.status !== 0)
|
|
2063
|
+
return;
|
|
2064
|
+
const root = result.stdout.trim();
|
|
2065
|
+
return root || undefined;
|
|
2066
|
+
}
|
|
2067
|
+
function topologyMetadata(input, existing = {}) {
|
|
2068
|
+
const next = { ...existing };
|
|
2069
|
+
const workspacePath = input.workspace_path ? resolve(input.workspace_path) : undefined;
|
|
2070
|
+
const entries = {
|
|
2071
|
+
tailscale_name: input.tailscale_name,
|
|
2072
|
+
tailscale_ip: input.tailscale_ip,
|
|
2073
|
+
lan_address: input.lan_address,
|
|
2074
|
+
workspace_path: workspacePath,
|
|
2075
|
+
git_root: input.git_root ?? (workspacePath ? discoverGitRoot(workspacePath) : undefined),
|
|
2076
|
+
arch: input.arch
|
|
2077
|
+
};
|
|
2078
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
2079
|
+
if (value !== undefined && value !== "")
|
|
2080
|
+
next[key] = value;
|
|
2081
|
+
}
|
|
2082
|
+
return next;
|
|
2083
|
+
}
|
|
2084
|
+
function extractTopology(machine) {
|
|
2085
|
+
const metadata = machine.metadata;
|
|
2086
|
+
return {
|
|
2087
|
+
tailscale_name: typeof metadata["tailscale_name"] === "string" ? metadata["tailscale_name"] : undefined,
|
|
2088
|
+
tailscale_ip: typeof metadata["tailscale_ip"] === "string" ? metadata["tailscale_ip"] : undefined,
|
|
2089
|
+
lan_address: typeof metadata["lan_address"] === "string" ? metadata["lan_address"] : undefined,
|
|
2090
|
+
workspace_path: typeof metadata["workspace_path"] === "string" ? metadata["workspace_path"] : undefined,
|
|
2091
|
+
git_root: typeof metadata["git_root"] === "string" ? metadata["git_root"] : undefined,
|
|
2092
|
+
arch: typeof metadata["arch"] === "string" ? metadata["arch"] : undefined
|
|
1743
2093
|
};
|
|
1744
2094
|
}
|
|
1745
2095
|
function getOrCreateLocalMachine(db) {
|
|
@@ -1783,6 +2133,159 @@ function listMachines(db, includeArchived = false) {
|
|
|
1783
2133
|
const rows = d.query(query).all();
|
|
1784
2134
|
return rows.map(rowToMachine);
|
|
1785
2135
|
}
|
|
2136
|
+
function registerMachine(name, opts, db) {
|
|
2137
|
+
const d = db || getDatabase();
|
|
2138
|
+
const existing = d.query("SELECT * FROM machines WHERE name = ?").get(name);
|
|
2139
|
+
const metadata = topologyMetadata(opts, parseMetadata(existing?.metadata ?? null));
|
|
2140
|
+
if (existing) {
|
|
2141
|
+
d.run("UPDATE machines SET hostname = ?, platform = ?, ssh_address = ?, last_seen_at = ?, metadata = ? WHERE id = ?", [
|
|
2142
|
+
opts.hostname ?? existing.hostname,
|
|
2143
|
+
opts.platform ?? existing.platform,
|
|
2144
|
+
opts.ssh_address ?? existing.ssh_address,
|
|
2145
|
+
now(),
|
|
2146
|
+
JSON.stringify(metadata),
|
|
2147
|
+
existing.id
|
|
2148
|
+
]);
|
|
2149
|
+
if (opts.primary) {
|
|
2150
|
+
setPrimaryMachine(name, d);
|
|
2151
|
+
}
|
|
2152
|
+
return getMachine(existing.id, d);
|
|
2153
|
+
}
|
|
2154
|
+
const id = uuid();
|
|
2155
|
+
const ts = now();
|
|
2156
|
+
const host = opts.hostname || osHostname();
|
|
2157
|
+
const plat = opts.platform || osPlatform();
|
|
2158
|
+
d.run("INSERT INTO machines (id, name, hostname, platform, ssh_address, last_seen_at, is_primary, metadata, created_at) VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)", [id, name, host, plat, opts.ssh_address ?? null, ts, JSON.stringify(metadata), ts]);
|
|
2159
|
+
if (opts.primary) {
|
|
2160
|
+
setPrimaryMachine(name, d);
|
|
2161
|
+
}
|
|
2162
|
+
return getMachine(id, d);
|
|
2163
|
+
}
|
|
2164
|
+
function updateMachineHeartbeat(idOrName, opts = {}, db) {
|
|
2165
|
+
const d = db || getDatabase();
|
|
2166
|
+
const key = idOrName || process.env["TODOS_MACHINE_NAME"] || osHostname();
|
|
2167
|
+
const row = d.query("SELECT * FROM machines WHERE id = ? OR name = ?").get(key, key);
|
|
2168
|
+
if (!row) {
|
|
2169
|
+
return registerMachine(key, {
|
|
2170
|
+
hostname: opts.hostname ?? osHostname(),
|
|
2171
|
+
platform: opts.platform ?? osPlatform(),
|
|
2172
|
+
arch: opts.arch ?? osArch(),
|
|
2173
|
+
...opts
|
|
2174
|
+
}, d);
|
|
2175
|
+
}
|
|
2176
|
+
const metadata = topologyMetadata({ arch: osArch(), ...opts }, parseMetadata(row.metadata));
|
|
2177
|
+
const ts = now();
|
|
2178
|
+
d.run("UPDATE machines SET hostname = ?, platform = ?, ssh_address = ?, last_seen_at = ?, metadata = ? WHERE id = ?", [
|
|
2179
|
+
opts.hostname ?? row.hostname ?? osHostname(),
|
|
2180
|
+
opts.platform ?? row.platform ?? osPlatform(),
|
|
2181
|
+
opts.ssh_address ?? row.ssh_address,
|
|
2182
|
+
ts,
|
|
2183
|
+
JSON.stringify(metadata),
|
|
2184
|
+
row.id
|
|
2185
|
+
]);
|
|
2186
|
+
return getMachine(row.id, d);
|
|
2187
|
+
}
|
|
2188
|
+
function getMachineTopologyDiagnostics(opts = {}, db, at = new Date) {
|
|
2189
|
+
const d = db || getDatabase();
|
|
2190
|
+
const staleAfter = opts.stale_minutes ?? 30;
|
|
2191
|
+
const generatedAt = at.toISOString();
|
|
2192
|
+
const localMachine = getOrCreateLocalMachine(d);
|
|
2193
|
+
const machines = listMachines(d, opts.include_archived ?? true);
|
|
2194
|
+
const machineById = new Map(machines.map((machine) => [machine.id, machine]));
|
|
2195
|
+
const summaries = machines.map((machine) => {
|
|
2196
|
+
const lastSeenMs = Date.parse(machine.last_seen_at);
|
|
2197
|
+
const staleMinutes = Number.isFinite(lastSeenMs) ? Math.max(0, Math.floor((at.getTime() - lastSeenMs) / 60000)) : Number.POSITIVE_INFINITY;
|
|
2198
|
+
return {
|
|
2199
|
+
id: machine.id,
|
|
2200
|
+
name: machine.name,
|
|
2201
|
+
hostname: machine.hostname,
|
|
2202
|
+
platform: machine.platform,
|
|
2203
|
+
ssh_address: machine.ssh_address,
|
|
2204
|
+
is_primary: machine.is_primary,
|
|
2205
|
+
archived_at: machine.archived_at,
|
|
2206
|
+
last_seen_at: machine.last_seen_at,
|
|
2207
|
+
stale: !machine.archived_at && staleMinutes > staleAfter,
|
|
2208
|
+
stale_minutes: Number.isFinite(staleMinutes) ? staleMinutes : staleAfter + 1,
|
|
2209
|
+
topology: extractTopology(machine)
|
|
2210
|
+
};
|
|
2211
|
+
});
|
|
2212
|
+
const pathRows = d.query(`SELECT p.id AS project_id, p.name AS project_name, pmp.machine_id, m.name AS machine_name, pmp.path
|
|
2213
|
+
FROM project_machine_paths pmp
|
|
2214
|
+
JOIN projects p ON p.id = pmp.project_id
|
|
2215
|
+
LEFT JOIN machines m ON m.id = pmp.machine_id
|
|
2216
|
+
ORDER BY p.name, pmp.machine_id`).all();
|
|
2217
|
+
const projectRows = d.query("SELECT id, name, path FROM projects ORDER BY name").all();
|
|
2218
|
+
const rowsByProject = new Map;
|
|
2219
|
+
for (const row of pathRows) {
|
|
2220
|
+
const rows = rowsByProject.get(row.project_id) ?? [];
|
|
2221
|
+
rows.push({ ...row, machine_name: row.machine_name ?? row.machine_id });
|
|
2222
|
+
rowsByProject.set(row.project_id, rows);
|
|
2223
|
+
}
|
|
2224
|
+
const pathIssues = [];
|
|
2225
|
+
for (const project of projectRows) {
|
|
2226
|
+
const rows = rowsByProject.get(project.id) ?? [];
|
|
2227
|
+
const localRow = rows.find((row) => row.machine_id === localMachine.id);
|
|
2228
|
+
if (!localRow) {
|
|
2229
|
+
pathIssues.push({
|
|
2230
|
+
type: "missing_local_path",
|
|
2231
|
+
project_id: project.id,
|
|
2232
|
+
project_name: project.name,
|
|
2233
|
+
message: `No machine-local path override is registered for ${project.name} on ${localMachine.name}`
|
|
2234
|
+
});
|
|
2235
|
+
}
|
|
2236
|
+
const distinctPaths = [...new Set(rows.map((row) => row.path))];
|
|
2237
|
+
if (distinctPaths.length > 1) {
|
|
2238
|
+
pathIssues.push({
|
|
2239
|
+
type: "path_mismatch",
|
|
2240
|
+
project_id: project.id,
|
|
2241
|
+
project_name: project.name,
|
|
2242
|
+
paths: rows.map((row) => ({ machine_id: row.machine_id, machine_name: row.machine_name, path: row.path })),
|
|
2243
|
+
message: `${project.name} has ${distinctPaths.length} different machine-local paths`
|
|
2244
|
+
});
|
|
2245
|
+
}
|
|
2246
|
+
if (localRow && !existsSync(localRow.path)) {
|
|
2247
|
+
pathIssues.push({
|
|
2248
|
+
type: "path_missing",
|
|
2249
|
+
project_id: project.id,
|
|
2250
|
+
project_name: project.name,
|
|
2251
|
+
machine_id: localMachine.id,
|
|
2252
|
+
machine_name: localMachine.name,
|
|
2253
|
+
path: localRow.path,
|
|
2254
|
+
message: `Local path does not exist on this machine: ${localRow.path}`
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
if (!localRow && project.path && machineById.has(localMachine.id) && !existsSync(project.path)) {
|
|
2258
|
+
pathIssues.push({
|
|
2259
|
+
type: "path_missing",
|
|
2260
|
+
project_id: project.id,
|
|
2261
|
+
project_name: project.name,
|
|
2262
|
+
machine_id: localMachine.id,
|
|
2263
|
+
machine_name: localMachine.name,
|
|
2264
|
+
path: project.path,
|
|
2265
|
+
message: `Project path does not exist on this machine: ${project.path}`
|
|
2266
|
+
});
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
return {
|
|
2270
|
+
generated_at: generatedAt,
|
|
2271
|
+
stale_after_minutes: staleAfter,
|
|
2272
|
+
local_machine: localMachine,
|
|
2273
|
+
machines: summaries,
|
|
2274
|
+
stale_machines: summaries.filter((summary) => summary.stale),
|
|
2275
|
+
path_issues: pathIssues
|
|
2276
|
+
};
|
|
2277
|
+
}
|
|
2278
|
+
function setPrimaryMachine(name, db) {
|
|
2279
|
+
const d = db || getDatabase();
|
|
2280
|
+
const row = d.query("SELECT * FROM machines WHERE name = ?").get(name);
|
|
2281
|
+
if (!row)
|
|
2282
|
+
throw new Error(`Machine '${name}' not found`);
|
|
2283
|
+
if (row.archived_at)
|
|
2284
|
+
throw new Error(`Cannot set archived machine '${name}' as primary`);
|
|
2285
|
+
d.run("UPDATE machines SET is_primary = 0 WHERE archived_at IS NULL");
|
|
2286
|
+
d.run("UPDATE machines SET is_primary = 1 WHERE id = ?", [row.id]);
|
|
2287
|
+
return rowToMachine({ ...row, is_primary: 1 });
|
|
2288
|
+
}
|
|
1786
2289
|
function deleteMachine(id, db) {
|
|
1787
2290
|
const d = db || getDatabase();
|
|
1788
2291
|
const row = d.query("SELECT * FROM machines WHERE id = ?").get(id);
|
|
@@ -1851,16 +2354,16 @@ __export(exports_database, {
|
|
|
1851
2354
|
LOCK_EXPIRY_MINUTES: () => LOCK_EXPIRY_MINUTES
|
|
1852
2355
|
});
|
|
1853
2356
|
import { Database } from "bun:sqlite";
|
|
1854
|
-
import { existsSync, mkdirSync } from "fs";
|
|
1855
|
-
import { dirname, join, resolve } from "path";
|
|
2357
|
+
import { existsSync as existsSync2, mkdirSync } from "fs";
|
|
2358
|
+
import { dirname, join, resolve as resolve2 } from "path";
|
|
1856
2359
|
function isInMemoryDb(path) {
|
|
1857
2360
|
return path === ":memory:" || path.startsWith("file::memory:");
|
|
1858
2361
|
}
|
|
1859
2362
|
function findNearestTodosDb(startDir) {
|
|
1860
|
-
let dir =
|
|
2363
|
+
let dir = resolve2(startDir);
|
|
1861
2364
|
while (true) {
|
|
1862
2365
|
const candidate = join(dir, ".todos", "todos.db");
|
|
1863
|
-
if (
|
|
2366
|
+
if (existsSync2(candidate))
|
|
1864
2367
|
return candidate;
|
|
1865
2368
|
const parent = dirname(dir);
|
|
1866
2369
|
if (parent === dir)
|
|
@@ -1870,9 +2373,9 @@ function findNearestTodosDb(startDir) {
|
|
|
1870
2373
|
return null;
|
|
1871
2374
|
}
|
|
1872
2375
|
function findGitRoot(startDir) {
|
|
1873
|
-
let dir =
|
|
2376
|
+
let dir = resolve2(startDir);
|
|
1874
2377
|
while (true) {
|
|
1875
|
-
if (
|
|
2378
|
+
if (existsSync2(join(dir, ".git")))
|
|
1876
2379
|
return dir;
|
|
1877
2380
|
const parent = dirname(dir);
|
|
1878
2381
|
if (parent === dir)
|
|
@@ -1901,7 +2404,7 @@ function getDbPath() {
|
|
|
1901
2404
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
1902
2405
|
const newPath = join(home, ".hasna", "todos", "todos.db");
|
|
1903
2406
|
const legacyPath = join(home, ".todos", "todos.db");
|
|
1904
|
-
if (!
|
|
2407
|
+
if (!existsSync2(newPath) && existsSync2(legacyPath)) {
|
|
1905
2408
|
return legacyPath;
|
|
1906
2409
|
}
|
|
1907
2410
|
return newPath;
|
|
@@ -1912,8 +2415,8 @@ function getDatabasePath() {
|
|
|
1912
2415
|
function ensureDir(filePath) {
|
|
1913
2416
|
if (isInMemoryDb(filePath))
|
|
1914
2417
|
return;
|
|
1915
|
-
const dir = dirname(
|
|
1916
|
-
if (!
|
|
2418
|
+
const dir = dirname(resolve2(filePath));
|
|
2419
|
+
if (!existsSync2(dir)) {
|
|
1917
2420
|
mkdirSync(dir, { recursive: true });
|
|
1918
2421
|
}
|
|
1919
2422
|
}
|
|
@@ -1998,7 +2501,7 @@ var LOCK_EXPIRY_MINUTES = 30, _db = null, ALLOWED_TABLES;
|
|
|
1998
2501
|
var init_database = __esm(() => {
|
|
1999
2502
|
init_schema();
|
|
2000
2503
|
init_machines();
|
|
2001
|
-
ALLOWED_TABLES = new Set(["tasks", "projects", "agents", "plans", "task_lists", "task_templates"]);
|
|
2504
|
+
ALLOWED_TABLES = new Set(["tasks", "projects", "agents", "plans", "task_lists", "task_templates", "project_knowledge_records", "project_risks", "local_retrospectives"]);
|
|
2002
2505
|
});
|
|
2003
2506
|
|
|
2004
2507
|
// src/db/task-crud.ts
|
|
@@ -2009,19 +2512,19 @@ init_database();
|
|
|
2009
2512
|
init_types();
|
|
2010
2513
|
|
|
2011
2514
|
// src/lib/config.ts
|
|
2012
|
-
import { existsSync as
|
|
2515
|
+
import { existsSync as existsSync4 } from "fs";
|
|
2013
2516
|
import { dirname as dirname2, join as join3 } from "path";
|
|
2014
2517
|
|
|
2015
2518
|
// src/lib/sync-utils.ts
|
|
2016
|
-
import { existsSync as
|
|
2519
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
|
|
2017
2520
|
import { join as join2 } from "path";
|
|
2018
2521
|
var HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
2019
2522
|
function ensureDir2(dir) {
|
|
2020
|
-
if (!
|
|
2523
|
+
if (!existsSync3(dir))
|
|
2021
2524
|
mkdirSync2(dir, { recursive: true });
|
|
2022
2525
|
}
|
|
2023
2526
|
function listJsonFiles(dir) {
|
|
2024
|
-
if (!
|
|
2527
|
+
if (!existsSync3(dir))
|
|
2025
2528
|
return [];
|
|
2026
2529
|
return readdirSync(dir).filter((f) => f.endsWith(".json"));
|
|
2027
2530
|
}
|
|
@@ -2038,7 +2541,7 @@ function writeJsonFile(path, data) {
|
|
|
2038
2541
|
}
|
|
2039
2542
|
function readHighWaterMark(dir) {
|
|
2040
2543
|
const path = join2(dir, ".highwatermark");
|
|
2041
|
-
if (!
|
|
2544
|
+
if (!existsSync3(path))
|
|
2042
2545
|
return 1;
|
|
2043
2546
|
const val = parseInt(readFileSync(path, "utf-8").trim(), 10);
|
|
2044
2547
|
return isNaN(val) ? 1 : val;
|
|
@@ -2072,7 +2575,7 @@ function getTodosGlobalDir() {
|
|
|
2072
2575
|
const legacyDir = join3(home, ".todos");
|
|
2073
2576
|
const newConfig = join3(newDir, "config.json");
|
|
2074
2577
|
const legacyConfig = join3(legacyDir, "config.json");
|
|
2075
|
-
if (!
|
|
2578
|
+
if (!existsSync4(newConfig) && existsSync4(legacyConfig))
|
|
2076
2579
|
return legacyDir;
|
|
2077
2580
|
return newDir;
|
|
2078
2581
|
}
|
|
@@ -2086,7 +2589,7 @@ function normalizeAgent(agent) {
|
|
|
2086
2589
|
function loadConfig() {
|
|
2087
2590
|
if (cached)
|
|
2088
2591
|
return cached;
|
|
2089
|
-
if (!
|
|
2592
|
+
if (!existsSync4(getConfigPath())) {
|
|
2090
2593
|
cached = {};
|
|
2091
2594
|
return cached;
|
|
2092
2595
|
}
|
|
@@ -2428,12 +2931,49 @@ function checkCompletionGuard(task, agentId, db, configOverride) {
|
|
|
2428
2931
|
// src/lib/event-hooks.ts
|
|
2429
2932
|
import { createHash, randomUUID } from "crypto";
|
|
2430
2933
|
import { appendFileSync, mkdirSync as mkdirSync3 } from "fs";
|
|
2431
|
-
import { dirname as dirname3, resolve as
|
|
2934
|
+
import { dirname as dirname3, resolve as resolve5 } from "path";
|
|
2432
2935
|
import { createConnection } from "net";
|
|
2433
2936
|
|
|
2434
2937
|
// src/lib/redaction.ts
|
|
2938
|
+
var DEFAULT_SECRET_PATTERNS = [
|
|
2939
|
+
{ name: "aws-access-key", regex: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g, replacement: "[REDACTED_AWS_KEY]" },
|
|
2940
|
+
{ name: "private-key", regex: /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g, replacement: "[REDACTED_PRIVATE_KEY]" },
|
|
2941
|
+
{ name: "openai-token", regex: /\bsk-[A-Za-z0-9_-]{12,}\b/g, replacement: "[REDACTED_TOKEN]" },
|
|
2942
|
+
{ name: "env-secret-assignment", regex: /\b([A-Za-z0-9_]*(?:API_KEY|TOKEN|SECRET|PASSWORD)[A-Za-z0-9_]*)\s*=\s*['"]?[^'"\s]{8,}/gi, replacement: "$1=[REDACTED]" },
|
|
2943
|
+
{ name: "bearer-token", regex: /\b(bearer)\s+[A-Za-z0-9._~+/=-]{12,}/gi, replacement: "$1 [REDACTED]" }
|
|
2944
|
+
];
|
|
2945
|
+
var DEFAULT_SECRET_KEY_PATTERN = /api[_-]?key|token|secret|password/i;
|
|
2946
|
+
function unique(values) {
|
|
2947
|
+
return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
|
|
2948
|
+
}
|
|
2949
|
+
function cloneRegex(regex) {
|
|
2950
|
+
return new RegExp(regex.source, regex.flags.includes("g") ? regex.flags : `${regex.flags}g`);
|
|
2951
|
+
}
|
|
2952
|
+
function customPatterns() {
|
|
2953
|
+
return unique(loadConfig().secret_safety?.redaction_patterns).flatMap((pattern) => {
|
|
2954
|
+
try {
|
|
2955
|
+
return [{ name: `custom:${pattern}`, regex: new RegExp(pattern, "g") }];
|
|
2956
|
+
} catch {
|
|
2957
|
+
return [];
|
|
2958
|
+
}
|
|
2959
|
+
});
|
|
2960
|
+
}
|
|
2961
|
+
function secretPatterns() {
|
|
2962
|
+
return [...customPatterns(), ...DEFAULT_SECRET_PATTERNS];
|
|
2963
|
+
}
|
|
2964
|
+
function isSecretKey(key) {
|
|
2965
|
+
if (DEFAULT_SECRET_KEY_PATTERN.test(key))
|
|
2966
|
+
return true;
|
|
2967
|
+
return unique(loadConfig().secret_safety?.redaction_keys).some((pattern) => key.toLowerCase().includes(pattern.toLowerCase()));
|
|
2968
|
+
}
|
|
2435
2969
|
function redactEvidenceText(value) {
|
|
2436
|
-
|
|
2970
|
+
let redacted = value;
|
|
2971
|
+
for (const pattern of secretPatterns()) {
|
|
2972
|
+
const regex = cloneRegex(pattern.regex);
|
|
2973
|
+
const replacement = pattern.replacement ?? "[REDACTED]";
|
|
2974
|
+
redacted = typeof replacement === "string" ? redacted.replace(regex, replacement) : redacted.replace(regex, replacement);
|
|
2975
|
+
}
|
|
2976
|
+
return redacted;
|
|
2437
2977
|
}
|
|
2438
2978
|
function redactValue(value) {
|
|
2439
2979
|
if (typeof value === "string")
|
|
@@ -2443,7 +2983,7 @@ function redactValue(value) {
|
|
|
2443
2983
|
if (value && typeof value === "object") {
|
|
2444
2984
|
const redacted = {};
|
|
2445
2985
|
for (const [key, child] of Object.entries(value)) {
|
|
2446
|
-
if (
|
|
2986
|
+
if (isSecretKey(key)) {
|
|
2447
2987
|
redacted[key] = "[REDACTED]";
|
|
2448
2988
|
} else {
|
|
2449
2989
|
redacted[key] = redactValue(child);
|
|
@@ -2453,12 +2993,39 @@ function redactValue(value) {
|
|
|
2453
2993
|
}
|
|
2454
2994
|
return value;
|
|
2455
2995
|
}
|
|
2996
|
+
function listSecretFindings(value) {
|
|
2997
|
+
const findings = [];
|
|
2998
|
+
for (const pattern of secretPatterns()) {
|
|
2999
|
+
const matches = value.match(cloneRegex(pattern.regex));
|
|
3000
|
+
if (matches?.length)
|
|
3001
|
+
findings.push({ pattern: pattern.name, count: matches.length });
|
|
3002
|
+
}
|
|
3003
|
+
return findings;
|
|
3004
|
+
}
|
|
3005
|
+
function hasSecretFindings(value) {
|
|
3006
|
+
return listSecretFindings(value).length > 0;
|
|
3007
|
+
}
|
|
3008
|
+
function getSecretSafetyConfig() {
|
|
3009
|
+
return {
|
|
3010
|
+
redaction_patterns: unique(loadConfig().secret_safety?.redaction_patterns),
|
|
3011
|
+
redaction_keys: unique(loadConfig().secret_safety?.redaction_keys)
|
|
3012
|
+
};
|
|
3013
|
+
}
|
|
3014
|
+
function upsertSecretSafetyConfig(input) {
|
|
3015
|
+
const config = loadConfig();
|
|
3016
|
+
const next = {
|
|
3017
|
+
redaction_patterns: unique([...config.secret_safety?.redaction_patterns || [], ...input.redaction_patterns || []]),
|
|
3018
|
+
redaction_keys: unique([...config.secret_safety?.redaction_keys || [], ...input.redaction_keys || []])
|
|
3019
|
+
};
|
|
3020
|
+
saveConfig({ ...config, secret_safety: next });
|
|
3021
|
+
return next;
|
|
3022
|
+
}
|
|
2456
3023
|
|
|
2457
3024
|
// src/lib/runner-sandbox.ts
|
|
2458
|
-
import { relative as relative2, resolve as
|
|
3025
|
+
import { relative as relative2, resolve as resolve4 } from "path";
|
|
2459
3026
|
|
|
2460
3027
|
// src/lib/workspace-trust.ts
|
|
2461
|
-
import { relative, resolve as
|
|
3028
|
+
import { relative, resolve as resolve3 } from "path";
|
|
2462
3029
|
var DEFAULT_DENYLIST = ["rm -rf", "mkfs", "dd if=", "curl | sh", "wget | sh"];
|
|
2463
3030
|
var DEFAULT_ENV_REDACTIONS = ["API_KEY", "TOKEN", "SECRET", "PASSWORD", "AUTH"];
|
|
2464
3031
|
var PRESET_DEFAULTS = {
|
|
@@ -2504,9 +3071,9 @@ var PRESET_DEFAULTS = {
|
|
|
2504
3071
|
}
|
|
2505
3072
|
};
|
|
2506
3073
|
function normalizePath(path) {
|
|
2507
|
-
return
|
|
3074
|
+
return resolve3(path);
|
|
2508
3075
|
}
|
|
2509
|
-
function
|
|
3076
|
+
function unique2(values) {
|
|
2510
3077
|
return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
|
|
2511
3078
|
}
|
|
2512
3079
|
function defaultProfile(root, preset) {
|
|
@@ -2566,11 +3133,11 @@ function upsertWorkspaceTrustProfile(input) {
|
|
|
2566
3133
|
root,
|
|
2567
3134
|
preset,
|
|
2568
3135
|
trusted: input.trusted ?? base.trusted ?? PRESET_DEFAULTS[preset].trusted,
|
|
2569
|
-
command_allowlist:
|
|
2570
|
-
command_denylist:
|
|
2571
|
-
tool_permissions:
|
|
2572
|
-
write_scopes:
|
|
2573
|
-
env_redactions:
|
|
3136
|
+
command_allowlist: unique2(input.command_allowlist ?? base.command_allowlist ?? PRESET_DEFAULTS[preset].command_allowlist),
|
|
3137
|
+
command_denylist: unique2(input.command_denylist ?? base.command_denylist ?? PRESET_DEFAULTS[preset].command_denylist),
|
|
3138
|
+
tool_permissions: unique2(input.tool_permissions ?? base.tool_permissions ?? PRESET_DEFAULTS[preset].tool_permissions),
|
|
3139
|
+
write_scopes: unique2(input.write_scopes ?? base.write_scopes ?? PRESET_DEFAULTS[preset].write_scopes),
|
|
3140
|
+
env_redactions: unique2(input.env_redactions ?? base.env_redactions ?? PRESET_DEFAULTS[preset].env_redactions),
|
|
2574
3141
|
require_prompt_for_unsafe: input.require_prompt_for_unsafe ?? base.require_prompt_for_unsafe ?? PRESET_DEFAULTS[preset].require_prompt_for_unsafe,
|
|
2575
3142
|
created_at: existing?.created_at || timestamp,
|
|
2576
3143
|
updated_at: timestamp
|
|
@@ -2604,7 +3171,7 @@ function writeAllowed(profile, root, writePath) {
|
|
|
2604
3171
|
function redactedEnvKeys(profile, env) {
|
|
2605
3172
|
if (!env)
|
|
2606
3173
|
return [];
|
|
2607
|
-
const patterns =
|
|
3174
|
+
const patterns = unique2([...DEFAULT_ENV_REDACTIONS, ...profile.env_redactions]).map((item) => item.toUpperCase());
|
|
2608
3175
|
return Object.keys(env).filter((key) => patterns.some((pattern) => key.toUpperCase().includes(pattern)));
|
|
2609
3176
|
}
|
|
2610
3177
|
function checkWorkspacePermission(input = {}) {
|
|
@@ -2641,9 +3208,9 @@ function checkWorkspacePermission(input = {}) {
|
|
|
2641
3208
|
var DEFAULT_COMMAND_DENYLIST = ["rm -rf", "mkfs", "dd if=", "curl | sh", "wget | sh"];
|
|
2642
3209
|
var DEFAULT_ENV_REDACTIONS2 = ["API_KEY", "TOKEN", "SECRET", "PASSWORD", "AUTH"];
|
|
2643
3210
|
function normalizePath2(path) {
|
|
2644
|
-
return
|
|
3211
|
+
return resolve4(path);
|
|
2645
3212
|
}
|
|
2646
|
-
function
|
|
3213
|
+
function unique3(values) {
|
|
2647
3214
|
return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
|
|
2648
3215
|
}
|
|
2649
3216
|
function configuredProfiles2(config = loadConfig()) {
|
|
@@ -2699,7 +3266,7 @@ function profileByName(name, path) {
|
|
|
2699
3266
|
function redactedEnvKeys2(profile, env) {
|
|
2700
3267
|
if (!env)
|
|
2701
3268
|
return [];
|
|
2702
|
-
const patterns =
|
|
3269
|
+
const patterns = unique3([...DEFAULT_ENV_REDACTIONS2, ...profile.env_redactions]).map((item) => item.toUpperCase());
|
|
2703
3270
|
return Object.keys(env).filter((key) => patterns.some((pattern) => key.toUpperCase().includes(pattern)));
|
|
2704
3271
|
}
|
|
2705
3272
|
function omittedEnvKeys(profile, env) {
|
|
@@ -2732,12 +3299,12 @@ function upsertRunnerSandboxProfile(input) {
|
|
|
2732
3299
|
...base,
|
|
2733
3300
|
name: input.name,
|
|
2734
3301
|
root,
|
|
2735
|
-
command_allowlist:
|
|
2736
|
-
command_denylist:
|
|
3302
|
+
command_allowlist: unique3(input.command_allowlist ?? base.command_allowlist),
|
|
3303
|
+
command_denylist: unique3(input.command_denylist ?? base.command_denylist),
|
|
2737
3304
|
cwd_boundary: normalizePath2(input.cwd_boundary || base.cwd_boundary || root),
|
|
2738
|
-
write_scopes:
|
|
2739
|
-
env_allowlist:
|
|
2740
|
-
env_redactions:
|
|
3305
|
+
write_scopes: unique3(input.write_scopes ?? base.write_scopes),
|
|
3306
|
+
env_allowlist: unique3(input.env_allowlist ?? base.env_allowlist),
|
|
3307
|
+
env_redactions: unique3(input.env_redactions ?? base.env_redactions),
|
|
2741
3308
|
network_policy: input.network_policy || base.network_policy,
|
|
2742
3309
|
require_approval: input.require_approval ?? base.require_approval,
|
|
2743
3310
|
audit_evidence: input.audit_evidence ?? base.audit_evidence,
|
|
@@ -2797,7 +3364,7 @@ function checkRunnerSandbox(input = {}) {
|
|
|
2797
3364
|
const redacted = redactedEnvKeys2(profile, input.env);
|
|
2798
3365
|
const omitted = omittedEnvKeys(profile, input.env);
|
|
2799
3366
|
const effective = Object.keys(input.env || {}).filter((key) => !omitted.includes(key));
|
|
2800
|
-
const uniqueReasons =
|
|
3367
|
+
const uniqueReasons = unique3(reasons);
|
|
2801
3368
|
const allowed = uniqueReasons.length === 0;
|
|
2802
3369
|
return {
|
|
2803
3370
|
allowed,
|
|
@@ -2830,9 +3397,14 @@ var LOCAL_EVENT_TYPES = [
|
|
|
2830
3397
|
"task.blocked",
|
|
2831
3398
|
"task.started",
|
|
2832
3399
|
"task.completed",
|
|
3400
|
+
"task.due",
|
|
3401
|
+
"task.due_soon",
|
|
2833
3402
|
"task.failed",
|
|
3403
|
+
"task.sla_breached",
|
|
3404
|
+
"task.stale",
|
|
2834
3405
|
"task.unblocked",
|
|
2835
3406
|
"task.status_changed",
|
|
3407
|
+
"calendar.reminder",
|
|
2836
3408
|
"plan.updated",
|
|
2837
3409
|
"run.started",
|
|
2838
3410
|
"run.completed",
|
|
@@ -2979,7 +3551,7 @@ async function deliverHook(hook, envelope) {
|
|
|
2979
3551
|
if (hook.target === "stdout") {
|
|
2980
3552
|
output = line.trim();
|
|
2981
3553
|
} else if (hook.target === "file") {
|
|
2982
|
-
const filePath =
|
|
3554
|
+
const filePath = resolve5(hook.file_path);
|
|
2983
3555
|
mkdirSync3(dirname3(filePath), { recursive: true });
|
|
2984
3556
|
appendFileSync(filePath, line);
|
|
2985
3557
|
} else if (hook.target === "socket") {
|
|
@@ -3414,8 +3986,8 @@ function createTask(input, db) {
|
|
|
3414
3986
|
let id = uuid();
|
|
3415
3987
|
for (let attempt = 0;attempt < 3; attempt++) {
|
|
3416
3988
|
try {
|
|
3417
|
-
d.run(`INSERT INTO tasks (id, short_id, project_id, parent_id, plan_id, task_list_id, cycle_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at, due_at, estimated_minutes, confidence, retry_count, max_retries, retry_after, requires_approval, approved_by, approved_at, recurrence_rule, recurrence_parent_id, spawns_template_id, reason, spawned_from_session, assigned_by, assigned_from_project, task_type)
|
|
3418
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
3989
|
+
d.run(`INSERT INTO tasks (id, short_id, project_id, parent_id, plan_id, task_list_id, cycle_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at, due_at, estimated_minutes, sla_minutes, confidence, retry_count, max_retries, retry_after, requires_approval, approved_by, approved_at, recurrence_rule, recurrence_parent_id, spawns_template_id, reason, spawned_from_session, assigned_by, assigned_from_project, task_type)
|
|
3990
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
3419
3991
|
id,
|
|
3420
3992
|
null,
|
|
3421
3993
|
input.project_id || null,
|
|
@@ -3437,6 +4009,7 @@ function createTask(input, db) {
|
|
|
3437
4009
|
timestamp,
|
|
3438
4010
|
input.due_at || null,
|
|
3439
4011
|
input.estimated_minutes || null,
|
|
4012
|
+
input.sla_minutes ?? null,
|
|
3440
4013
|
input.confidence ?? null,
|
|
3441
4014
|
input.retry_count ?? 0,
|
|
3442
4015
|
input.max_retries ?? 3,
|
|
@@ -3744,6 +4317,10 @@ function updateTask(id, input, db) {
|
|
|
3744
4317
|
sets.push("estimated_minutes = ?");
|
|
3745
4318
|
params.push(input.estimated_minutes);
|
|
3746
4319
|
}
|
|
4320
|
+
if (input.sla_minutes !== undefined) {
|
|
4321
|
+
sets.push("sla_minutes = ?");
|
|
4322
|
+
params.push(input.sla_minutes);
|
|
4323
|
+
}
|
|
3747
4324
|
if (input.actual_minutes !== undefined) {
|
|
3748
4325
|
sets.push("actual_minutes = ?");
|
|
3749
4326
|
params.push(input.actual_minutes);
|
|
@@ -3825,6 +4402,7 @@ function updateTask(id, input, db) {
|
|
|
3825
4402
|
version: task.version + 1,
|
|
3826
4403
|
updated_at: timestamp,
|
|
3827
4404
|
completed_at: input.status === "completed" ? completionTimestamp : input.completed_at !== undefined ? input.completed_at : task.completed_at,
|
|
4405
|
+
sla_minutes: input.sla_minutes !== undefined ? input.sla_minutes : task.sla_minutes,
|
|
3828
4406
|
actual_minutes: input.actual_minutes ?? task.actual_minutes,
|
|
3829
4407
|
confidence: input.confidence !== undefined ? input.confidence : task.confidence,
|
|
3830
4408
|
retry_count: input.retry_count ?? task.retry_count,
|
|
@@ -4608,7 +5186,7 @@ function completeTask(id, agentId, db, options) {
|
|
|
4608
5186
|
emitLocalEventHooksQuiet({ type: "task.completed", payload: { id, agent_id: agentId, title: task.title, completed_at: timestamp } });
|
|
4609
5187
|
let spawnedTask = null;
|
|
4610
5188
|
if (task.recurrence_rule && !options?.skip_recurrence) {
|
|
4611
|
-
spawnedTask = spawnNextRecurrence(task, d);
|
|
5189
|
+
spawnedTask = spawnNextRecurrence(task, d, timestamp);
|
|
4612
5190
|
}
|
|
4613
5191
|
let spawnedFromTemplate = null;
|
|
4614
5192
|
if (task.spawns_template_id) {
|
|
@@ -4938,8 +5516,9 @@ function claimOrSteal(agentId, filters, db) {
|
|
|
4938
5516
|
});
|
|
4939
5517
|
return tx();
|
|
4940
5518
|
}
|
|
4941
|
-
function spawnNextRecurrence(completedTask, db) {
|
|
4942
|
-
const
|
|
5519
|
+
function spawnNextRecurrence(completedTask, db, completedAt) {
|
|
5520
|
+
const recurrenceBase = completedTask.due_at ? new Date(completedTask.due_at) : new Date(completedAt);
|
|
5521
|
+
const dueAt = nextOccurrence(completedTask.recurrence_rule, recurrenceBase);
|
|
4943
5522
|
let title = completedTask.title;
|
|
4944
5523
|
if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
|
|
4945
5524
|
title = title.slice(completedTask.short_id.length + 2);
|
|
@@ -4956,6 +5535,7 @@ function spawnNextRecurrence(completedTask, db) {
|
|
|
4956
5535
|
tags: completedTask.tags,
|
|
4957
5536
|
metadata: completedTask.metadata,
|
|
4958
5537
|
estimated_minutes: completedTask.estimated_minutes ?? undefined,
|
|
5538
|
+
sla_minutes: completedTask.sla_minutes ?? undefined,
|
|
4959
5539
|
recurrence_rule: completedTask.recurrence_rule,
|
|
4960
5540
|
recurrence_parent_id: recurrenceParentId,
|
|
4961
5541
|
due_at: dueAt
|
|
@@ -5208,10 +5788,10 @@ function unarchiveTask(id, db) {
|
|
|
5208
5788
|
d.run("UPDATE tasks SET archived_at = NULL WHERE id = ?", [id]);
|
|
5209
5789
|
return getTask(id, d);
|
|
5210
5790
|
}
|
|
5211
|
-
function getOverdueTasks(projectId, db) {
|
|
5791
|
+
function getOverdueTasks(projectId, db, at = new Date) {
|
|
5212
5792
|
const d = db || getDatabase();
|
|
5213
|
-
const nowStr =
|
|
5214
|
-
let query = `SELECT * FROM tasks WHERE due_at IS NOT NULL AND due_at < ? AND status NOT IN ('completed', 'cancelled', 'failed')`;
|
|
5793
|
+
const nowStr = at.toISOString();
|
|
5794
|
+
let query = `SELECT * FROM tasks WHERE archived_at IS NULL AND due_at IS NOT NULL AND due_at < ? AND status NOT IN ('completed', 'cancelled', 'failed')`;
|
|
5215
5795
|
const params = [nowStr];
|
|
5216
5796
|
if (projectId) {
|
|
5217
5797
|
query += ` AND project_id = ?`;
|
|
@@ -5221,21 +5801,298 @@ function getOverdueTasks(projectId, db) {
|
|
|
5221
5801
|
const rows = d.query(query).all(...params);
|
|
5222
5802
|
return rows.map(rowToTask);
|
|
5223
5803
|
}
|
|
5224
|
-
function
|
|
5804
|
+
function getEscalatedTasks(opts = {}, db, at = new Date) {
|
|
5225
5805
|
const d = db || getDatabase();
|
|
5226
|
-
|
|
5806
|
+
const nowMs = at.getTime();
|
|
5807
|
+
const conditions = [
|
|
5808
|
+
"archived_at IS NULL",
|
|
5809
|
+
"status NOT IN ('completed', 'cancelled', 'failed')",
|
|
5810
|
+
"(due_at IS NOT NULL OR sla_minutes IS NOT NULL)"
|
|
5811
|
+
];
|
|
5812
|
+
const params = [];
|
|
5813
|
+
if (opts.project_id) {
|
|
5814
|
+
conditions.push("project_id = ?");
|
|
5815
|
+
params.push(opts.project_id);
|
|
5816
|
+
}
|
|
5817
|
+
if (opts.agent_id) {
|
|
5818
|
+
conditions.push("assigned_to = ?");
|
|
5819
|
+
params.push(opts.agent_id);
|
|
5820
|
+
}
|
|
5821
|
+
const rows = d.query(`SELECT * FROM tasks WHERE ${conditions.join(" AND ")} ORDER BY due_at ASC, created_at ASC`).all(...params);
|
|
5822
|
+
return rows.map(rowToTask).map((task) => {
|
|
5823
|
+
const reasons = [];
|
|
5824
|
+
const breachedTimes = [];
|
|
5825
|
+
if (task.due_at) {
|
|
5826
|
+
const dueMs = new Date(task.due_at).getTime();
|
|
5827
|
+
if (Number.isFinite(dueMs) && dueMs < nowMs) {
|
|
5828
|
+
reasons.push("overdue");
|
|
5829
|
+
breachedTimes.push(dueMs);
|
|
5830
|
+
}
|
|
5831
|
+
}
|
|
5832
|
+
if (task.sla_minutes != null) {
|
|
5833
|
+
const startMs = new Date(task.started_at ?? task.created_at).getTime();
|
|
5834
|
+
const breachedMs = startMs + task.sla_minutes * 60000;
|
|
5835
|
+
if (Number.isFinite(breachedMs) && breachedMs < nowMs) {
|
|
5836
|
+
reasons.push("sla_breached");
|
|
5837
|
+
breachedTimes.push(breachedMs);
|
|
5838
|
+
}
|
|
5839
|
+
}
|
|
5840
|
+
if (reasons.length === 0)
|
|
5841
|
+
return null;
|
|
5842
|
+
return {
|
|
5843
|
+
task,
|
|
5844
|
+
reasons,
|
|
5845
|
+
breached_at: new Date(Math.min(...breachedTimes)).toISOString()
|
|
5846
|
+
};
|
|
5847
|
+
}).filter((item) => item !== null);
|
|
5227
5848
|
}
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5849
|
+
function rowToFocusSession(row) {
|
|
5850
|
+
return {
|
|
5851
|
+
...row,
|
|
5852
|
+
metadata: JSON.parse(row.metadata || "{}")
|
|
5853
|
+
};
|
|
5854
|
+
}
|
|
5855
|
+
function minutesBetween(start, end) {
|
|
5856
|
+
if (!start || !end)
|
|
5857
|
+
return 0;
|
|
5858
|
+
const startMs = Date.parse(start);
|
|
5859
|
+
const endMs = Date.parse(end);
|
|
5860
|
+
if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs <= startMs)
|
|
5861
|
+
return 0;
|
|
5862
|
+
return Math.max(1, Math.ceil((endMs - startMs) / 60000));
|
|
5863
|
+
}
|
|
5864
|
+
function assertPositiveMinutes(minutes) {
|
|
5865
|
+
if (!Number.isFinite(minutes) || minutes < 1)
|
|
5866
|
+
throw new Error("minutes must be a positive integer");
|
|
5867
|
+
return Math.floor(minutes);
|
|
5868
|
+
}
|
|
5869
|
+
function recalculateTaskActualMinutes(taskId, db) {
|
|
5870
|
+
const row = db.query("SELECT COALESCE(SUM(minutes), 0) as total FROM task_time_logs WHERE task_id = ?").get(taskId);
|
|
5871
|
+
const total = Number(row.total || 0);
|
|
5872
|
+
db.run("UPDATE tasks SET actual_minutes = ?, updated_at = ? WHERE id = ?", [total, now(), taskId]);
|
|
5873
|
+
return total;
|
|
5874
|
+
}
|
|
5875
|
+
function logTime(input, db) {
|
|
5232
5876
|
const d = db || getDatabase();
|
|
5877
|
+
if (!getTask(input.task_id, d))
|
|
5878
|
+
throw new Error(`Task not found: ${input.task_id}`);
|
|
5233
5879
|
const id = uuid();
|
|
5234
|
-
const
|
|
5235
|
-
|
|
5236
|
-
|
|
5880
|
+
const ts = now();
|
|
5881
|
+
const minutes = assertPositiveMinutes(input.minutes);
|
|
5882
|
+
d.run(`INSERT INTO task_time_logs (id, task_id, run_id, focus_session_id, agent_id, minutes, started_at, ended_at, notes, created_at)
|
|
5883
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.task_id, input.run_id || null, input.focus_session_id || null, input.agent_id || null, minutes, input.started_at || null, input.ended_at || null, input.notes || null, ts]);
|
|
5884
|
+
recalculateTaskActualMinutes(input.task_id, d);
|
|
5885
|
+
return {
|
|
5237
5886
|
id,
|
|
5238
|
-
input.
|
|
5887
|
+
task_id: input.task_id,
|
|
5888
|
+
run_id: input.run_id || null,
|
|
5889
|
+
focus_session_id: input.focus_session_id || null,
|
|
5890
|
+
agent_id: input.agent_id || null,
|
|
5891
|
+
minutes,
|
|
5892
|
+
started_at: input.started_at || null,
|
|
5893
|
+
ended_at: input.ended_at || null,
|
|
5894
|
+
notes: input.notes || null,
|
|
5895
|
+
created_at: ts
|
|
5896
|
+
};
|
|
5897
|
+
}
|
|
5898
|
+
function getTimeLogs(taskId, db) {
|
|
5899
|
+
const d = db || getDatabase();
|
|
5900
|
+
return d.query(`SELECT * FROM task_time_logs WHERE task_id = ? ORDER BY created_at DESC`).all(taskId);
|
|
5901
|
+
}
|
|
5902
|
+
function startFocusSession(input, db) {
|
|
5903
|
+
const d = db || getDatabase();
|
|
5904
|
+
if (input.task_id && !getTask(input.task_id, d))
|
|
5905
|
+
throw new Error(`Task not found: ${input.task_id}`);
|
|
5906
|
+
const id = uuid();
|
|
5907
|
+
const ts = now();
|
|
5908
|
+
const startedAt = input.started_at || ts;
|
|
5909
|
+
d.run(`INSERT INTO focus_sessions (
|
|
5910
|
+
id, task_id, plan_id, run_id, agent_id, title, status, started_at, last_resumed_at,
|
|
5911
|
+
actual_minutes, idle_after_minutes, notes, metadata, created_at, updated_at
|
|
5912
|
+
) VALUES (?, ?, ?, ?, ?, ?, 'active', ?, ?, 0, ?, ?, ?, ?, ?)`, [
|
|
5913
|
+
id,
|
|
5914
|
+
input.task_id || null,
|
|
5915
|
+
input.plan_id || null,
|
|
5916
|
+
input.run_id || null,
|
|
5917
|
+
input.agent_id || null,
|
|
5918
|
+
input.title || null,
|
|
5919
|
+
startedAt,
|
|
5920
|
+
startedAt,
|
|
5921
|
+
input.idle_after_minutes ?? null,
|
|
5922
|
+
input.notes || null,
|
|
5923
|
+
JSON.stringify(input.metadata || {}),
|
|
5924
|
+
ts,
|
|
5925
|
+
ts
|
|
5926
|
+
]);
|
|
5927
|
+
return getFocusSession(id, d);
|
|
5928
|
+
}
|
|
5929
|
+
function getFocusSession(id, db) {
|
|
5930
|
+
const d = db || getDatabase();
|
|
5931
|
+
const row = d.query("SELECT * FROM focus_sessions WHERE id = ?").get(id);
|
|
5932
|
+
return row ? rowToFocusSession(row) : null;
|
|
5933
|
+
}
|
|
5934
|
+
function listFocusSessions(query = {}, db) {
|
|
5935
|
+
const d = db || getDatabase();
|
|
5936
|
+
const conditions = [];
|
|
5937
|
+
const params = [];
|
|
5938
|
+
if (query.task_id) {
|
|
5939
|
+
conditions.push("task_id = ?");
|
|
5940
|
+
params.push(query.task_id);
|
|
5941
|
+
}
|
|
5942
|
+
if (query.plan_id) {
|
|
5943
|
+
conditions.push("plan_id = ?");
|
|
5944
|
+
params.push(query.plan_id);
|
|
5945
|
+
}
|
|
5946
|
+
if (query.run_id) {
|
|
5947
|
+
conditions.push("run_id = ?");
|
|
5948
|
+
params.push(query.run_id);
|
|
5949
|
+
}
|
|
5950
|
+
if (query.agent_id) {
|
|
5951
|
+
conditions.push("agent_id = ?");
|
|
5952
|
+
params.push(query.agent_id);
|
|
5953
|
+
}
|
|
5954
|
+
if (query.status) {
|
|
5955
|
+
conditions.push("status = ?");
|
|
5956
|
+
params.push(query.status);
|
|
5957
|
+
} else if (!query.include_completed) {
|
|
5958
|
+
conditions.push("status IN ('active', 'paused')");
|
|
5959
|
+
}
|
|
5960
|
+
let sql = "SELECT * FROM focus_sessions";
|
|
5961
|
+
if (conditions.length > 0)
|
|
5962
|
+
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
5963
|
+
sql += " ORDER BY updated_at DESC";
|
|
5964
|
+
if (query.limit) {
|
|
5965
|
+
sql += " LIMIT ?";
|
|
5966
|
+
params.push(query.limit);
|
|
5967
|
+
}
|
|
5968
|
+
return d.query(sql).all(...params).map(rowToFocusSession);
|
|
5969
|
+
}
|
|
5970
|
+
function pauseFocusSession(id, pausedAt, db) {
|
|
5971
|
+
const d = db || getDatabase();
|
|
5972
|
+
const session = getFocusSession(id, d);
|
|
5973
|
+
if (!session)
|
|
5974
|
+
throw new Error(`Focus session not found: ${id}`);
|
|
5975
|
+
if (session.status !== "active")
|
|
5976
|
+
throw new Error(`Focus session is ${session.status}, not active`);
|
|
5977
|
+
const ts = now();
|
|
5978
|
+
const pauseAt = pausedAt || ts;
|
|
5979
|
+
const minutes = session.actual_minutes + minutesBetween(session.last_resumed_at, pauseAt);
|
|
5980
|
+
d.run("UPDATE focus_sessions SET status = 'paused', actual_minutes = ?, paused_at = ?, last_resumed_at = NULL, updated_at = ? WHERE id = ?", [minutes, pauseAt, ts, id]);
|
|
5981
|
+
return getFocusSession(id, d);
|
|
5982
|
+
}
|
|
5983
|
+
function resumeFocusSession(id, resumedAt, db) {
|
|
5984
|
+
const d = db || getDatabase();
|
|
5985
|
+
const session = getFocusSession(id, d);
|
|
5986
|
+
if (!session)
|
|
5987
|
+
throw new Error(`Focus session not found: ${id}`);
|
|
5988
|
+
if (session.status !== "paused")
|
|
5989
|
+
throw new Error(`Focus session is ${session.status}, not paused`);
|
|
5990
|
+
const ts = now();
|
|
5991
|
+
const resumed = resumedAt || ts;
|
|
5992
|
+
d.run("UPDATE focus_sessions SET status = 'active', paused_at = NULL, last_resumed_at = ?, updated_at = ? WHERE id = ?", [resumed, ts, id]);
|
|
5993
|
+
return getFocusSession(id, d);
|
|
5994
|
+
}
|
|
5995
|
+
function stopFocusSession(input, db) {
|
|
5996
|
+
const d = db || getDatabase();
|
|
5997
|
+
const session = getFocusSession(input.id, d);
|
|
5998
|
+
if (!session)
|
|
5999
|
+
throw new Error(`Focus session not found: ${input.id}`);
|
|
6000
|
+
if (session.status === "completed" || session.status === "cancelled")
|
|
6001
|
+
return session;
|
|
6002
|
+
const ts = now();
|
|
6003
|
+
const endedAt = input.ended_at || ts;
|
|
6004
|
+
const finalMinutes = session.status === "active" ? session.actual_minutes + minutesBetween(session.last_resumed_at, endedAt) : session.actual_minutes;
|
|
6005
|
+
const finalStatus = input.status || "completed";
|
|
6006
|
+
d.run("UPDATE focus_sessions SET status = ?, actual_minutes = ?, ended_at = ?, last_resumed_at = NULL, paused_at = NULL, notes = COALESCE(?, notes), updated_at = ? WHERE id = ?", [finalStatus, finalMinutes, endedAt, input.notes || null, ts, input.id]);
|
|
6007
|
+
const stopped = getFocusSession(input.id, d);
|
|
6008
|
+
if (stopped.task_id && finalStatus === "completed" && finalMinutes > 0) {
|
|
6009
|
+
logTime({
|
|
6010
|
+
task_id: stopped.task_id,
|
|
6011
|
+
run_id: stopped.run_id || undefined,
|
|
6012
|
+
focus_session_id: stopped.id,
|
|
6013
|
+
agent_id: stopped.agent_id || undefined,
|
|
6014
|
+
minutes: finalMinutes,
|
|
6015
|
+
started_at: stopped.started_at,
|
|
6016
|
+
ended_at: stopped.ended_at || endedAt,
|
|
6017
|
+
notes: input.notes || stopped.notes || undefined
|
|
6018
|
+
}, d);
|
|
6019
|
+
}
|
|
6020
|
+
return stopped;
|
|
6021
|
+
}
|
|
6022
|
+
function getIdleFocusSessionPrompts(opts = {}, db) {
|
|
6023
|
+
const d = db || getDatabase();
|
|
6024
|
+
const nowDate = opts.now ? new Date(opts.now) : new Date;
|
|
6025
|
+
const sessions = listFocusSessions({ agent_id: opts.agent_id, status: "active", include_completed: false }, d);
|
|
6026
|
+
return sessions.flatMap((session) => {
|
|
6027
|
+
if (!session.idle_after_minutes || !session.last_resumed_at)
|
|
6028
|
+
return [];
|
|
6029
|
+
const idleMinutes = Math.floor((nowDate.getTime() - Date.parse(session.last_resumed_at)) / 60000);
|
|
6030
|
+
if (!Number.isFinite(idleMinutes) || idleMinutes < session.idle_after_minutes)
|
|
6031
|
+
return [];
|
|
6032
|
+
return [{
|
|
6033
|
+
session,
|
|
6034
|
+
idle_minutes: idleMinutes,
|
|
6035
|
+
message: `Focus session ${session.id.slice(0, 8)} has been active for ${idleMinutes} minutes. Pause, stop, or continue it.`
|
|
6036
|
+
}];
|
|
6037
|
+
});
|
|
6038
|
+
}
|
|
6039
|
+
function getTimeReport(opts, db) {
|
|
6040
|
+
const d = db || getDatabase();
|
|
6041
|
+
const conditions = opts?.include_open ? ["1 = 1"] : ["t.status = 'completed'"];
|
|
6042
|
+
const params = [];
|
|
6043
|
+
if (opts?.project_id) {
|
|
6044
|
+
conditions.push("t.project_id = ?");
|
|
6045
|
+
params.push(opts.project_id);
|
|
6046
|
+
}
|
|
6047
|
+
if (opts?.plan_id) {
|
|
6048
|
+
conditions.push("t.plan_id = ?");
|
|
6049
|
+
params.push(opts.plan_id);
|
|
6050
|
+
}
|
|
6051
|
+
if (opts?.agent_id) {
|
|
6052
|
+
conditions.push(`(
|
|
6053
|
+
t.assigned_to = ?
|
|
6054
|
+
OR t.agent_id = ?
|
|
6055
|
+
OR EXISTS (SELECT 1 FROM task_time_logs ttl WHERE ttl.task_id = t.id AND ttl.agent_id = ?)
|
|
6056
|
+
OR EXISTS (SELECT 1 FROM focus_sessions fs WHERE fs.task_id = t.id AND fs.agent_id = ?)
|
|
6057
|
+
)`);
|
|
6058
|
+
params.push(opts.agent_id, opts.agent_id, opts.agent_id, opts.agent_id);
|
|
6059
|
+
}
|
|
6060
|
+
if (opts?.since) {
|
|
6061
|
+
conditions.push("(t.completed_at >= ? OR t.updated_at >= ?)");
|
|
6062
|
+
params.push(opts.since, opts.since);
|
|
6063
|
+
}
|
|
6064
|
+
const rows = d.query(`
|
|
6065
|
+
SELECT t.id as task_id, t.title, t.project_id, t.plan_id, t.estimated_minutes, t.actual_minutes
|
|
6066
|
+
FROM tasks t
|
|
6067
|
+
WHERE ${conditions.join(" AND ")}
|
|
6068
|
+
ORDER BY t.completed_at DESC, t.updated_at DESC
|
|
6069
|
+
`).all(...params);
|
|
6070
|
+
return rows.map((row) => {
|
|
6071
|
+
const timeLogs = getTimeLogs(row.task_id, d);
|
|
6072
|
+
const focusSessions = listFocusSessions({ task_id: row.task_id, include_completed: true }, d);
|
|
6073
|
+
const loggedMinutes = timeLogs.reduce((acc, log) => acc + log.minutes, 0);
|
|
6074
|
+
const focusMinutes = focusSessions.reduce((acc, session) => acc + session.actual_minutes, 0);
|
|
6075
|
+
return { ...row, logged_minutes: loggedMinutes, focus_minutes: focusMinutes, time_logs: timeLogs, focus_sessions: focusSessions };
|
|
6076
|
+
});
|
|
6077
|
+
}
|
|
6078
|
+
function logCost(taskId, tokens, usd, db) {
|
|
6079
|
+
const d = db || getDatabase();
|
|
6080
|
+
d.run("UPDATE tasks SET cost_tokens = cost_tokens + ?, cost_usd = cost_usd + ?, updated_at = ? WHERE id = ?", [tokens, usd, now(), taskId]);
|
|
6081
|
+
}
|
|
6082
|
+
// src/db/boards.ts
|
|
6083
|
+
init_database();
|
|
6084
|
+
|
|
6085
|
+
// src/db/plans.ts
|
|
6086
|
+
init_types();
|
|
6087
|
+
init_database();
|
|
6088
|
+
function createPlan(input, db) {
|
|
6089
|
+
const d = db || getDatabase();
|
|
6090
|
+
const id = uuid();
|
|
6091
|
+
const timestamp = now();
|
|
6092
|
+
d.run(`INSERT INTO plans (id, project_id, task_list_id, agent_id, name, description, status, created_at, updated_at)
|
|
6093
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
6094
|
+
id,
|
|
6095
|
+
input.project_id || null,
|
|
5239
6096
|
input.task_list_id || null,
|
|
5240
6097
|
input.agent_id || null,
|
|
5241
6098
|
input.name,
|
|
@@ -5244,62 +6101,1459 @@ function createPlan(input, db) {
|
|
|
5244
6101
|
timestamp,
|
|
5245
6102
|
timestamp
|
|
5246
6103
|
]);
|
|
5247
|
-
return getPlan(id, d);
|
|
6104
|
+
return getPlan(id, d);
|
|
6105
|
+
}
|
|
6106
|
+
function getPlan(id, db) {
|
|
6107
|
+
const d = db || getDatabase();
|
|
6108
|
+
const row = d.query("SELECT * FROM plans WHERE id = ?").get(id);
|
|
6109
|
+
return row;
|
|
6110
|
+
}
|
|
6111
|
+
function listPlans(projectId, db) {
|
|
6112
|
+
const d = db || getDatabase();
|
|
6113
|
+
if (projectId) {
|
|
6114
|
+
return d.query("SELECT * FROM plans WHERE project_id = ? ORDER BY created_at DESC").all(projectId);
|
|
6115
|
+
}
|
|
6116
|
+
return d.query("SELECT * FROM plans ORDER BY created_at DESC").all();
|
|
6117
|
+
}
|
|
6118
|
+
function updatePlan(id, input, db) {
|
|
6119
|
+
const d = db || getDatabase();
|
|
6120
|
+
const plan = getPlan(id, d);
|
|
6121
|
+
if (!plan)
|
|
6122
|
+
throw new PlanNotFoundError(id);
|
|
6123
|
+
const sets = ["updated_at = ?"];
|
|
6124
|
+
const params = [now()];
|
|
6125
|
+
if (input.name !== undefined) {
|
|
6126
|
+
sets.push("name = ?");
|
|
6127
|
+
params.push(input.name);
|
|
6128
|
+
}
|
|
6129
|
+
if (input.description !== undefined) {
|
|
6130
|
+
sets.push("description = ?");
|
|
6131
|
+
params.push(input.description);
|
|
6132
|
+
}
|
|
6133
|
+
if (input.status !== undefined) {
|
|
6134
|
+
sets.push("status = ?");
|
|
6135
|
+
params.push(input.status);
|
|
6136
|
+
}
|
|
6137
|
+
if (input.task_list_id !== undefined) {
|
|
6138
|
+
sets.push("task_list_id = ?");
|
|
6139
|
+
params.push(input.task_list_id);
|
|
6140
|
+
}
|
|
6141
|
+
if (input.agent_id !== undefined) {
|
|
6142
|
+
sets.push("agent_id = ?");
|
|
6143
|
+
params.push(input.agent_id);
|
|
6144
|
+
}
|
|
6145
|
+
params.push(id);
|
|
6146
|
+
d.run(`UPDATE plans SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
6147
|
+
const updated = getPlan(id, d);
|
|
6148
|
+
emitLocalEventHooksQuiet({
|
|
6149
|
+
type: "plan.updated",
|
|
6150
|
+
payload: { id, old_status: plan.status, new_status: updated.status, name: updated.name, project_id: updated.project_id }
|
|
6151
|
+
});
|
|
6152
|
+
return updated;
|
|
6153
|
+
}
|
|
6154
|
+
function deletePlan(id, db) {
|
|
6155
|
+
const d = db || getDatabase();
|
|
6156
|
+
const result = d.run("DELETE FROM plans WHERE id = ?", [id]);
|
|
6157
|
+
return result.changes > 0;
|
|
6158
|
+
}
|
|
6159
|
+
|
|
6160
|
+
// src/db/boards.ts
|
|
6161
|
+
var TASK_LANES = [
|
|
6162
|
+
{ id: "ready", name: "Ready", statuses: ["pending"], wip_limit: null, position: 0 },
|
|
6163
|
+
{ id: "doing", name: "Doing", statuses: ["in_progress"], wip_limit: 3, position: 1 },
|
|
6164
|
+
{ id: "review", name: "Review", statuses: ["failed"], wip_limit: 5, position: 2 },
|
|
6165
|
+
{ id: "done", name: "Done", statuses: ["completed"], wip_limit: null, position: 3 },
|
|
6166
|
+
{ id: "cancelled", name: "Cancelled", statuses: ["cancelled"], wip_limit: null, position: 4 }
|
|
6167
|
+
];
|
|
6168
|
+
var PLAN_LANES = [
|
|
6169
|
+
{ id: "active", name: "Active", statuses: ["active"], wip_limit: 3, position: 0 },
|
|
6170
|
+
{ id: "completed", name: "Completed", statuses: ["completed"], wip_limit: null, position: 1 },
|
|
6171
|
+
{ id: "archived", name: "Archived", statuses: ["archived"], wip_limit: null, position: 2 }
|
|
6172
|
+
];
|
|
6173
|
+
function parseJsonObject(value) {
|
|
6174
|
+
if (!value)
|
|
6175
|
+
return {};
|
|
6176
|
+
if (typeof value === "object" && !Array.isArray(value))
|
|
6177
|
+
return value;
|
|
6178
|
+
if (typeof value !== "string")
|
|
6179
|
+
return {};
|
|
6180
|
+
try {
|
|
6181
|
+
const parsed = JSON.parse(value);
|
|
6182
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
6183
|
+
} catch {
|
|
6184
|
+
return {};
|
|
6185
|
+
}
|
|
6186
|
+
}
|
|
6187
|
+
function parseJsonArray(value) {
|
|
6188
|
+
if (Array.isArray(value))
|
|
6189
|
+
return value;
|
|
6190
|
+
if (typeof value !== "string")
|
|
6191
|
+
return [];
|
|
6192
|
+
try {
|
|
6193
|
+
const parsed = JSON.parse(value);
|
|
6194
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
6195
|
+
} catch {
|
|
6196
|
+
return [];
|
|
6197
|
+
}
|
|
6198
|
+
}
|
|
6199
|
+
function defaultLanes(scope) {
|
|
6200
|
+
return (scope === "plans" ? PLAN_LANES : TASK_LANES).map((lane) => ({ ...lane, statuses: [...lane.statuses] }));
|
|
6201
|
+
}
|
|
6202
|
+
function normalizeLanes(scope, lanes) {
|
|
6203
|
+
const source = lanes && lanes.length > 0 ? lanes : defaultLanes(scope);
|
|
6204
|
+
return source.map((lane, index) => ({
|
|
6205
|
+
id: lane.id || lane.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || `lane-${index + 1}`,
|
|
6206
|
+
name: lane.name || lane.id || `Lane ${index + 1}`,
|
|
6207
|
+
statuses: Array.from(new Set((lane.statuses || []).map(String).filter(Boolean))),
|
|
6208
|
+
wip_limit: lane.wip_limit === undefined ? null : lane.wip_limit,
|
|
6209
|
+
position: lane.position ?? index
|
|
6210
|
+
})).filter((lane) => lane.statuses.length > 0).sort((a, b) => a.position - b.position);
|
|
6211
|
+
}
|
|
6212
|
+
function rowToTaskBoard(row) {
|
|
6213
|
+
return {
|
|
6214
|
+
...row,
|
|
6215
|
+
lanes: normalizeLanes(row.scope, parseJsonArray(row.lanes)),
|
|
6216
|
+
filters: parseJsonObject(row.filters)
|
|
6217
|
+
};
|
|
6218
|
+
}
|
|
6219
|
+
function maybeFilter(value) {
|
|
6220
|
+
return value === undefined || value === null || value === "" ? undefined : value;
|
|
6221
|
+
}
|
|
6222
|
+
function createTaskBoard(input, db) {
|
|
6223
|
+
const d = db || getDatabase();
|
|
6224
|
+
const id = uuid();
|
|
6225
|
+
const timestamp = now();
|
|
6226
|
+
const scope = input.scope || "tasks";
|
|
6227
|
+
const lanes = normalizeLanes(scope, input.lanes);
|
|
6228
|
+
d.run(`INSERT INTO task_boards (id, name, scope, project_id, task_list_id, plan_id, agent_id, lanes, filters, created_at, updated_at)
|
|
6229
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
6230
|
+
id,
|
|
6231
|
+
input.name,
|
|
6232
|
+
scope,
|
|
6233
|
+
input.project_id || null,
|
|
6234
|
+
input.task_list_id || null,
|
|
6235
|
+
input.plan_id || null,
|
|
6236
|
+
input.agent_id || null,
|
|
6237
|
+
JSON.stringify(lanes),
|
|
6238
|
+
JSON.stringify(input.filters || {}),
|
|
6239
|
+
timestamp,
|
|
6240
|
+
timestamp
|
|
6241
|
+
]);
|
|
6242
|
+
return getTaskBoard(id, d);
|
|
6243
|
+
}
|
|
6244
|
+
function getTaskBoard(idOrName, db) {
|
|
6245
|
+
const d = db || getDatabase();
|
|
6246
|
+
const row = d.query("SELECT * FROM task_boards WHERE id = ? OR name = ?").get(idOrName, idOrName);
|
|
6247
|
+
return row ? rowToTaskBoard(row) : null;
|
|
6248
|
+
}
|
|
6249
|
+
function listTaskBoards(query = {}, db) {
|
|
6250
|
+
const d = db || getDatabase();
|
|
6251
|
+
const conditions = [];
|
|
6252
|
+
const params = [];
|
|
6253
|
+
if (query.scope) {
|
|
6254
|
+
conditions.push("scope = ?");
|
|
6255
|
+
params.push(query.scope);
|
|
6256
|
+
}
|
|
6257
|
+
if (query.project_id) {
|
|
6258
|
+
conditions.push("project_id = ?");
|
|
6259
|
+
params.push(query.project_id);
|
|
6260
|
+
}
|
|
6261
|
+
if (query.task_list_id) {
|
|
6262
|
+
conditions.push("task_list_id = ?");
|
|
6263
|
+
params.push(query.task_list_id);
|
|
6264
|
+
}
|
|
6265
|
+
if (query.plan_id) {
|
|
6266
|
+
conditions.push("plan_id = ?");
|
|
6267
|
+
params.push(query.plan_id);
|
|
6268
|
+
}
|
|
6269
|
+
if (query.agent_id) {
|
|
6270
|
+
conditions.push("agent_id = ?");
|
|
6271
|
+
params.push(query.agent_id);
|
|
6272
|
+
}
|
|
6273
|
+
let sql = "SELECT * FROM task_boards";
|
|
6274
|
+
if (conditions.length > 0)
|
|
6275
|
+
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
6276
|
+
sql += " ORDER BY updated_at DESC, name";
|
|
6277
|
+
if (query.limit) {
|
|
6278
|
+
sql += " LIMIT ?";
|
|
6279
|
+
params.push(query.limit);
|
|
6280
|
+
}
|
|
6281
|
+
return d.query(sql).all(...params).map(rowToTaskBoard);
|
|
6282
|
+
}
|
|
6283
|
+
function updateTaskBoard(idOrName, input, db) {
|
|
6284
|
+
const d = db || getDatabase();
|
|
6285
|
+
const board = getTaskBoard(idOrName, d);
|
|
6286
|
+
if (!board)
|
|
6287
|
+
throw new Error(`Board not found: ${idOrName}`);
|
|
6288
|
+
const sets = ["updated_at = ?"];
|
|
6289
|
+
const params = [now()];
|
|
6290
|
+
if (input.name !== undefined) {
|
|
6291
|
+
sets.push("name = ?");
|
|
6292
|
+
params.push(input.name);
|
|
6293
|
+
}
|
|
6294
|
+
if (input.project_id !== undefined) {
|
|
6295
|
+
sets.push("project_id = ?");
|
|
6296
|
+
params.push(input.project_id);
|
|
6297
|
+
}
|
|
6298
|
+
if (input.task_list_id !== undefined) {
|
|
6299
|
+
sets.push("task_list_id = ?");
|
|
6300
|
+
params.push(input.task_list_id);
|
|
6301
|
+
}
|
|
6302
|
+
if (input.plan_id !== undefined) {
|
|
6303
|
+
sets.push("plan_id = ?");
|
|
6304
|
+
params.push(input.plan_id);
|
|
6305
|
+
}
|
|
6306
|
+
if (input.agent_id !== undefined) {
|
|
6307
|
+
sets.push("agent_id = ?");
|
|
6308
|
+
params.push(input.agent_id);
|
|
6309
|
+
}
|
|
6310
|
+
if (input.lanes !== undefined) {
|
|
6311
|
+
sets.push("lanes = ?");
|
|
6312
|
+
params.push(JSON.stringify(normalizeLanes(board.scope, input.lanes)));
|
|
6313
|
+
}
|
|
6314
|
+
if (input.filters !== undefined) {
|
|
6315
|
+
sets.push("filters = ?");
|
|
6316
|
+
params.push(JSON.stringify(input.filters));
|
|
6317
|
+
}
|
|
6318
|
+
params.push(board.id);
|
|
6319
|
+
d.run(`UPDATE task_boards SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
6320
|
+
return getTaskBoard(board.id, d);
|
|
6321
|
+
}
|
|
6322
|
+
function deleteTaskBoard(idOrName, db) {
|
|
6323
|
+
const d = db || getDatabase();
|
|
6324
|
+
const board = getTaskBoard(idOrName, d);
|
|
6325
|
+
if (!board)
|
|
6326
|
+
return false;
|
|
6327
|
+
return d.run("DELETE FROM task_boards WHERE id = ?", [board.id]).changes > 0;
|
|
6328
|
+
}
|
|
6329
|
+
function incompleteDependencyCount(taskId, db) {
|
|
6330
|
+
const row = db.query(`SELECT COUNT(*) as count
|
|
6331
|
+
FROM task_dependencies td
|
|
6332
|
+
JOIN tasks dep ON dep.id = td.depends_on
|
|
6333
|
+
WHERE td.task_id = ? AND dep.status NOT IN ('completed', 'cancelled')`).get(taskId);
|
|
6334
|
+
return row.count;
|
|
6335
|
+
}
|
|
6336
|
+
function taskToCard(task, db) {
|
|
6337
|
+
const blockedCount = incompleteDependencyCount(task.id, db);
|
|
6338
|
+
const blocked = blockedCount > 0;
|
|
6339
|
+
const badges = [task.priority];
|
|
6340
|
+
if (blocked)
|
|
6341
|
+
badges.push("blocked");
|
|
6342
|
+
if (!blocked && task.status === "pending")
|
|
6343
|
+
badges.push("ready");
|
|
6344
|
+
if (task.assigned_to)
|
|
6345
|
+
badges.push(`@${task.assigned_to}`);
|
|
6346
|
+
if (task.due_at && Date.parse(task.due_at) < Date.now() && !["completed", "cancelled", "failed"].includes(task.status)) {
|
|
6347
|
+
badges.push("overdue");
|
|
6348
|
+
}
|
|
6349
|
+
return {
|
|
6350
|
+
id: task.id,
|
|
6351
|
+
short_id: task.short_id,
|
|
6352
|
+
title: task.title,
|
|
6353
|
+
status: task.status,
|
|
6354
|
+
priority: task.priority,
|
|
6355
|
+
project_id: task.project_id,
|
|
6356
|
+
plan_id: task.plan_id,
|
|
6357
|
+
task_list_id: task.task_list_id,
|
|
6358
|
+
assigned_to: task.assigned_to,
|
|
6359
|
+
blocked,
|
|
6360
|
+
ready: !blocked && task.status === "pending",
|
|
6361
|
+
badges,
|
|
6362
|
+
updated_at: task.updated_at
|
|
6363
|
+
};
|
|
6364
|
+
}
|
|
6365
|
+
function planTaskStats(planId, db) {
|
|
6366
|
+
const tasks = listTasks({ plan_id: planId, include_archived: true }, db);
|
|
6367
|
+
let blocked = 0;
|
|
6368
|
+
let ready = 0;
|
|
6369
|
+
for (const task of tasks) {
|
|
6370
|
+
const isBlocked = incompleteDependencyCount(task.id, db) > 0;
|
|
6371
|
+
if (isBlocked)
|
|
6372
|
+
blocked++;
|
|
6373
|
+
if (!isBlocked && task.status === "pending")
|
|
6374
|
+
ready++;
|
|
6375
|
+
}
|
|
6376
|
+
return {
|
|
6377
|
+
total: tasks.length,
|
|
6378
|
+
active: tasks.filter((task) => !["completed", "cancelled", "failed"].includes(task.status)).length,
|
|
6379
|
+
blocked,
|
|
6380
|
+
ready
|
|
6381
|
+
};
|
|
6382
|
+
}
|
|
6383
|
+
function planToCard(plan, db) {
|
|
6384
|
+
const stats = planTaskStats(plan.id, db);
|
|
6385
|
+
const badges = [`tasks:${stats.total}`, `active:${stats.active}`];
|
|
6386
|
+
if (stats.blocked > 0)
|
|
6387
|
+
badges.push(`blocked:${stats.blocked}`);
|
|
6388
|
+
if (stats.ready > 0)
|
|
6389
|
+
badges.push(`ready:${stats.ready}`);
|
|
6390
|
+
return {
|
|
6391
|
+
id: plan.id,
|
|
6392
|
+
short_id: null,
|
|
6393
|
+
title: plan.name,
|
|
6394
|
+
status: plan.status,
|
|
6395
|
+
priority: null,
|
|
6396
|
+
project_id: plan.project_id,
|
|
6397
|
+
plan_id: plan.id,
|
|
6398
|
+
task_list_id: plan.task_list_id,
|
|
6399
|
+
assigned_to: plan.agent_id,
|
|
6400
|
+
blocked: stats.blocked > 0,
|
|
6401
|
+
ready: stats.ready > 0,
|
|
6402
|
+
badges,
|
|
6403
|
+
updated_at: plan.updated_at
|
|
6404
|
+
};
|
|
6405
|
+
}
|
|
6406
|
+
function filteredTasks(board, db) {
|
|
6407
|
+
const filters = board.filters;
|
|
6408
|
+
return listTasks({
|
|
6409
|
+
project_id: board.project_id || maybeFilter(filters.project_id),
|
|
6410
|
+
task_list_id: board.task_list_id || maybeFilter(filters.task_list_id),
|
|
6411
|
+
plan_id: board.plan_id || maybeFilter(filters.plan_id),
|
|
6412
|
+
assigned_to: board.agent_id || maybeFilter(filters.assigned_to),
|
|
6413
|
+
agent_id: maybeFilter(filters.agent_id),
|
|
6414
|
+
priority: maybeFilter(filters.priority),
|
|
6415
|
+
tags: Array.isArray(filters.tags) ? filters.tags.map(String) : undefined,
|
|
6416
|
+
task_type: maybeFilter(filters.task_type),
|
|
6417
|
+
include_archived: Boolean(filters.include_archived)
|
|
6418
|
+
}, db);
|
|
6419
|
+
}
|
|
6420
|
+
function filteredPlans(board, db) {
|
|
6421
|
+
return listPlans(board.project_id || undefined, db).filter((plan) => !board.task_list_id || plan.task_list_id === board.task_list_id).filter((plan) => !board.agent_id || plan.agent_id === board.agent_id);
|
|
6422
|
+
}
|
|
6423
|
+
function buildLaneSnapshots(board, cards) {
|
|
6424
|
+
return board.lanes.map((lane) => {
|
|
6425
|
+
const laneCards = cards.filter((card) => lane.statuses.includes(card.status));
|
|
6426
|
+
return {
|
|
6427
|
+
lane,
|
|
6428
|
+
count: laneCards.length,
|
|
6429
|
+
wip_limit: lane.wip_limit,
|
|
6430
|
+
wip_exceeded: lane.wip_limit !== null && laneCards.length > lane.wip_limit,
|
|
6431
|
+
cards: laneCards
|
|
6432
|
+
};
|
|
6433
|
+
});
|
|
6434
|
+
}
|
|
6435
|
+
function buildTaskBoardSnapshot(idOrBoard, db) {
|
|
6436
|
+
const d = db || getDatabase();
|
|
6437
|
+
const board = typeof idOrBoard === "string" ? getTaskBoard(idOrBoard, d) : idOrBoard;
|
|
6438
|
+
if (!board)
|
|
6439
|
+
throw new Error(`Board not found: ${idOrBoard}`);
|
|
6440
|
+
const cards = board.scope === "plans" ? filteredPlans(board, d).map((plan) => planToCard(plan, d)) : filteredTasks(board, d).map((task) => taskToCard(task, d));
|
|
6441
|
+
const lanes = buildLaneSnapshots(board, cards);
|
|
6442
|
+
return {
|
|
6443
|
+
board,
|
|
6444
|
+
generated_at: now(),
|
|
6445
|
+
lanes,
|
|
6446
|
+
totals: {
|
|
6447
|
+
cards: cards.length,
|
|
6448
|
+
blocked: cards.filter((card) => card.blocked).length,
|
|
6449
|
+
ready: cards.filter((card) => card.ready).length,
|
|
6450
|
+
wip_exceeded_lanes: lanes.filter((lane) => lane.wip_exceeded).length
|
|
6451
|
+
},
|
|
6452
|
+
keyboard: {
|
|
6453
|
+
move_left: "h",
|
|
6454
|
+
move_right: "l",
|
|
6455
|
+
move_up: "k",
|
|
6456
|
+
move_down: "j",
|
|
6457
|
+
open: "enter",
|
|
6458
|
+
quit: "q"
|
|
6459
|
+
}
|
|
6460
|
+
};
|
|
6461
|
+
}
|
|
6462
|
+
function moveBoardCard(input, db) {
|
|
6463
|
+
const d = db || getDatabase();
|
|
6464
|
+
const board = getTaskBoard(input.board_id, d);
|
|
6465
|
+
if (!board)
|
|
6466
|
+
throw new Error(`Board not found: ${input.board_id}`);
|
|
6467
|
+
const lane = input.lane_id ? board.lanes.find((candidate) => candidate.id === input.lane_id || candidate.name === input.lane_id) : null;
|
|
6468
|
+
const status = input.status || lane?.statuses[0];
|
|
6469
|
+
if (!status)
|
|
6470
|
+
throw new Error("Target lane or status is required");
|
|
6471
|
+
if (board.scope === "plans") {
|
|
6472
|
+
const planId = resolvePartialId(d, "plans", input.card_id) || input.card_id;
|
|
6473
|
+
const plan = updatePlan(planId, { status }, d);
|
|
6474
|
+
return planToCard(plan, d);
|
|
6475
|
+
}
|
|
6476
|
+
const taskId = resolvePartialId(d, "tasks", input.card_id) || input.card_id;
|
|
6477
|
+
const task = getTask(taskId, d);
|
|
6478
|
+
if (!task)
|
|
6479
|
+
throw new Error(`Task not found: ${input.card_id}`);
|
|
6480
|
+
const updated = updateTask(task.id, { status, version: task.version }, d);
|
|
6481
|
+
return taskToCard(updated, d);
|
|
6482
|
+
}
|
|
6483
|
+
function renderTaskBoard(snapshot) {
|
|
6484
|
+
const lines = [
|
|
6485
|
+
`${snapshot.board.name} (${snapshot.board.scope})`,
|
|
6486
|
+
`cards:${snapshot.totals.cards} ready:${snapshot.totals.ready} blocked:${snapshot.totals.blocked} wip_exceeded:${snapshot.totals.wip_exceeded_lanes}`,
|
|
6487
|
+
`keys: ${snapshot.keyboard.move_left}/${snapshot.keyboard.move_right} lane, ${snapshot.keyboard.move_up}/${snapshot.keyboard.move_down} card, ${snapshot.keyboard.open} open, ${snapshot.keyboard.quit} quit`,
|
|
6488
|
+
""
|
|
6489
|
+
];
|
|
6490
|
+
for (const lane of snapshot.lanes) {
|
|
6491
|
+
const limit = lane.wip_limit === null ? "" : ` / ${lane.wip_limit}`;
|
|
6492
|
+
const marker = lane.wip_exceeded ? " !" : "";
|
|
6493
|
+
lines.push(`${lane.lane.name} (${lane.count}${limit})${marker}`);
|
|
6494
|
+
for (const card of lane.cards) {
|
|
6495
|
+
const id = card.short_id || card.id.slice(0, 8);
|
|
6496
|
+
lines.push(` ${id} ${card.title} [${card.badges.join(", ")}]`);
|
|
6497
|
+
}
|
|
6498
|
+
if (lane.cards.length === 0)
|
|
6499
|
+
lines.push(" (empty)");
|
|
6500
|
+
lines.push("");
|
|
6501
|
+
}
|
|
6502
|
+
return lines.join(`
|
|
6503
|
+
`).trimEnd();
|
|
6504
|
+
}
|
|
6505
|
+
function exportTaskBoardBundle(idOrName, db) {
|
|
6506
|
+
const d = db || getDatabase();
|
|
6507
|
+
const boards = idOrName ? [getTaskBoard(idOrName, d)].filter(Boolean) : listTaskBoards({}, d);
|
|
6508
|
+
return {
|
|
6509
|
+
kind: "hasna.todos.task-board",
|
|
6510
|
+
schemaVersion: 1,
|
|
6511
|
+
exportedAt: now(),
|
|
6512
|
+
boards
|
|
6513
|
+
};
|
|
6514
|
+
}
|
|
6515
|
+
function importTaskBoardBundle(bundle, db) {
|
|
6516
|
+
const d = db || getDatabase();
|
|
6517
|
+
if (bundle.kind !== "hasna.todos.task-board" || bundle.schemaVersion !== 1 || !Array.isArray(bundle.boards)) {
|
|
6518
|
+
throw new Error("Invalid task board bundle");
|
|
6519
|
+
}
|
|
6520
|
+
let inserted = 0;
|
|
6521
|
+
let updated = 0;
|
|
6522
|
+
let skipped = 0;
|
|
6523
|
+
for (const board of bundle.boards) {
|
|
6524
|
+
const existing = getTaskBoard(board.id, d) || getTaskBoard(board.name, d);
|
|
6525
|
+
if (existing) {
|
|
6526
|
+
updateTaskBoard(existing.id, {
|
|
6527
|
+
name: board.name,
|
|
6528
|
+
project_id: board.project_id,
|
|
6529
|
+
task_list_id: board.task_list_id,
|
|
6530
|
+
plan_id: board.plan_id,
|
|
6531
|
+
agent_id: board.agent_id,
|
|
6532
|
+
lanes: board.lanes,
|
|
6533
|
+
filters: board.filters
|
|
6534
|
+
}, d);
|
|
6535
|
+
updated++;
|
|
6536
|
+
} else if (board.name && board.scope) {
|
|
6537
|
+
createTaskBoard({
|
|
6538
|
+
name: board.name,
|
|
6539
|
+
scope: board.scope,
|
|
6540
|
+
project_id: board.project_id || undefined,
|
|
6541
|
+
task_list_id: board.task_list_id || undefined,
|
|
6542
|
+
plan_id: board.plan_id || undefined,
|
|
6543
|
+
agent_id: board.agent_id || undefined,
|
|
6544
|
+
lanes: board.lanes,
|
|
6545
|
+
filters: board.filters
|
|
6546
|
+
}, d);
|
|
6547
|
+
inserted++;
|
|
6548
|
+
} else {
|
|
6549
|
+
skipped++;
|
|
6550
|
+
}
|
|
6551
|
+
}
|
|
6552
|
+
return { inserted, updated, skipped };
|
|
6553
|
+
}
|
|
6554
|
+
// src/db/calendar.ts
|
|
6555
|
+
init_database();
|
|
6556
|
+
|
|
6557
|
+
// src/lib/artifact-store.ts
|
|
6558
|
+
init_database();
|
|
6559
|
+
import { createHash as createHash2 } from "crypto";
|
|
6560
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
6561
|
+
import { dirname as dirname4, join as join4, resolve as resolve6 } from "path";
|
|
6562
|
+
import { tmpdir } from "os";
|
|
6563
|
+
function isInMemoryDb2(path) {
|
|
6564
|
+
return path === ":memory:" || path.startsWith("file::memory:");
|
|
6565
|
+
}
|
|
6566
|
+
function artifactStoreRoot() {
|
|
6567
|
+
if (process.env["HASNA_TODOS_ARTIFACTS_DIR"])
|
|
6568
|
+
return resolve6(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
|
|
6569
|
+
if (process.env["TODOS_ARTIFACTS_DIR"])
|
|
6570
|
+
return resolve6(process.env["TODOS_ARTIFACTS_DIR"]);
|
|
6571
|
+
const dbPath = getDatabasePath();
|
|
6572
|
+
if (isInMemoryDb2(dbPath))
|
|
6573
|
+
return join4(tmpdir(), "hasna-todos-artifacts");
|
|
6574
|
+
return join4(dirname4(resolve6(dbPath)), "artifacts");
|
|
6575
|
+
}
|
|
6576
|
+
function artifactStorePath(relativePath) {
|
|
6577
|
+
const normalized = relativePath.replace(/\\/g, "/");
|
|
6578
|
+
if (normalized.includes("..") || normalized.startsWith("/") || normalized.length === 0) {
|
|
6579
|
+
throw new Error("Invalid artifact store path");
|
|
6580
|
+
}
|
|
6581
|
+
return join4(artifactStoreRoot(), normalized);
|
|
6582
|
+
}
|
|
6583
|
+
function sha256(buffer) {
|
|
6584
|
+
return createHash2("sha256").update(buffer).digest("hex");
|
|
6585
|
+
}
|
|
6586
|
+
function isTextLike(buffer, path) {
|
|
6587
|
+
if (buffer.includes(0))
|
|
6588
|
+
return false;
|
|
6589
|
+
if (/\.(txt|log|json|jsonl|md|csv|yaml|yml|xml|html|css|js|ts|tsx|jsx|patch|diff)$/i.test(path))
|
|
6590
|
+
return true;
|
|
6591
|
+
const sample = buffer.subarray(0, Math.min(buffer.length, 4096)).toString("utf8");
|
|
6592
|
+
return !sample.includes("\uFFFD");
|
|
6593
|
+
}
|
|
6594
|
+
function retentionExpiresAt(createdAt, retentionDays) {
|
|
6595
|
+
if (retentionDays === undefined)
|
|
6596
|
+
return null;
|
|
6597
|
+
if (!Number.isFinite(retentionDays) || retentionDays < 0)
|
|
6598
|
+
throw new Error("retention_days must be a non-negative number");
|
|
6599
|
+
const expires = new Date(createdAt);
|
|
6600
|
+
expires.setUTCDate(expires.getUTCDate() + retentionDays);
|
|
6601
|
+
return expires.toISOString();
|
|
6602
|
+
}
|
|
6603
|
+
function mediaTypeFor(path, textLike) {
|
|
6604
|
+
if (/\.(png)$/i.test(path))
|
|
6605
|
+
return "image/png";
|
|
6606
|
+
if (/\.(jpe?g)$/i.test(path))
|
|
6607
|
+
return "image/jpeg";
|
|
6608
|
+
if (/\.(gif)$/i.test(path))
|
|
6609
|
+
return "image/gif";
|
|
6610
|
+
if (/\.(webp)$/i.test(path))
|
|
6611
|
+
return "image/webp";
|
|
6612
|
+
if (/\.(json)$/i.test(path))
|
|
6613
|
+
return "application/json";
|
|
6614
|
+
if (/\.(md)$/i.test(path))
|
|
6615
|
+
return "text/markdown";
|
|
6616
|
+
if (textLike)
|
|
6617
|
+
return "text/plain";
|
|
6618
|
+
return "application/octet-stream";
|
|
6619
|
+
}
|
|
6620
|
+
function storeArtifactContent(input) {
|
|
6621
|
+
const sourcePath = resolve6(input.path);
|
|
6622
|
+
if (!existsSync5(sourcePath))
|
|
6623
|
+
return null;
|
|
6624
|
+
const sourceStat = statSync2(sourcePath);
|
|
6625
|
+
if (!sourceStat.isFile())
|
|
6626
|
+
throw new Error(`Artifact path is not a file: ${input.path}`);
|
|
6627
|
+
const sourceBuffer = readFileSync2(sourcePath);
|
|
6628
|
+
const sourceSha = sha256(sourceBuffer);
|
|
6629
|
+
const textLike = isTextLike(sourceBuffer, input.path);
|
|
6630
|
+
let storedBuffer = sourceBuffer;
|
|
6631
|
+
let redactionStatus = textLike ? "clean" : "binary_or_unknown";
|
|
6632
|
+
if (textLike) {
|
|
6633
|
+
const redactedText = redactEvidenceText(sourceBuffer.toString("utf8"));
|
|
6634
|
+
storedBuffer = Buffer.from(redactedText);
|
|
6635
|
+
if (!storedBuffer.equals(sourceBuffer))
|
|
6636
|
+
redactionStatus = "redacted";
|
|
6637
|
+
}
|
|
6638
|
+
const storedSha = sha256(storedBuffer);
|
|
6639
|
+
const relativePath = join4("sha256", storedSha.slice(0, 2), storedSha).replace(/\\/g, "/");
|
|
6640
|
+
const destination = artifactStorePath(relativePath);
|
|
6641
|
+
if (!existsSync5(destination)) {
|
|
6642
|
+
mkdirSync4(dirname4(destination), { recursive: true });
|
|
6643
|
+
writeFileSync2(destination, storedBuffer);
|
|
6644
|
+
}
|
|
6645
|
+
const createdAt = input.created_at || new Date().toISOString();
|
|
6646
|
+
const retentionDays = input.retention_days ?? null;
|
|
6647
|
+
return {
|
|
6648
|
+
size_bytes: storedBuffer.length,
|
|
6649
|
+
sha256: storedSha,
|
|
6650
|
+
store: {
|
|
6651
|
+
stored: true,
|
|
6652
|
+
algorithm: "sha256",
|
|
6653
|
+
sha256: storedSha,
|
|
6654
|
+
size_bytes: storedBuffer.length,
|
|
6655
|
+
relative_path: relativePath,
|
|
6656
|
+
content_address: `sha256:${storedSha}`,
|
|
6657
|
+
media_type: mediaTypeFor(input.path, textLike),
|
|
6658
|
+
redaction: {
|
|
6659
|
+
checked: textLike,
|
|
6660
|
+
status: redactionStatus
|
|
6661
|
+
},
|
|
6662
|
+
retention: {
|
|
6663
|
+
days: retentionDays,
|
|
6664
|
+
expires_at: retentionDays === null ? null : retentionExpiresAt(createdAt, retentionDays)
|
|
6665
|
+
},
|
|
6666
|
+
source: {
|
|
6667
|
+
path: input.path,
|
|
6668
|
+
size_bytes: sourceBuffer.length,
|
|
6669
|
+
sha256: sourceSha
|
|
6670
|
+
}
|
|
6671
|
+
}
|
|
6672
|
+
};
|
|
6673
|
+
}
|
|
6674
|
+
function storeMetadata(metadata) {
|
|
6675
|
+
const value = metadata["artifact_store"];
|
|
6676
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
6677
|
+
return null;
|
|
6678
|
+
const record = value;
|
|
6679
|
+
if (record.stored !== true)
|
|
6680
|
+
return null;
|
|
6681
|
+
if (typeof record.sha256 !== "string" || typeof record.relative_path !== "string" || typeof record.size_bytes !== "number")
|
|
6682
|
+
return null;
|
|
6683
|
+
return record;
|
|
6684
|
+
}
|
|
6685
|
+
function verifyStoredArtifact(input) {
|
|
6686
|
+
const store = storeMetadata(input.metadata);
|
|
6687
|
+
if (!store) {
|
|
6688
|
+
return {
|
|
6689
|
+
id: input.id,
|
|
6690
|
+
path: input.path,
|
|
6691
|
+
status: "metadata_only",
|
|
6692
|
+
expected_sha256: input.sha256,
|
|
6693
|
+
actual_sha256: null,
|
|
6694
|
+
expected_size_bytes: input.size_bytes,
|
|
6695
|
+
actual_size_bytes: null,
|
|
6696
|
+
relative_path: null,
|
|
6697
|
+
message: "artifact has metadata only and no local stored content"
|
|
6698
|
+
};
|
|
6699
|
+
}
|
|
6700
|
+
const storedPath = artifactStorePath(store.relative_path);
|
|
6701
|
+
if (!existsSync5(storedPath)) {
|
|
6702
|
+
return {
|
|
6703
|
+
id: input.id,
|
|
6704
|
+
path: input.path,
|
|
6705
|
+
status: "missing",
|
|
6706
|
+
expected_sha256: store.sha256,
|
|
6707
|
+
actual_sha256: null,
|
|
6708
|
+
expected_size_bytes: store.size_bytes,
|
|
6709
|
+
actual_size_bytes: null,
|
|
6710
|
+
relative_path: store.relative_path,
|
|
6711
|
+
message: "stored artifact content is missing"
|
|
6712
|
+
};
|
|
6713
|
+
}
|
|
6714
|
+
const buffer = readFileSync2(storedPath);
|
|
6715
|
+
const actualSha = sha256(buffer);
|
|
6716
|
+
const actualSize = buffer.length;
|
|
6717
|
+
const ok = actualSha === store.sha256 && actualSize === store.size_bytes;
|
|
6718
|
+
return {
|
|
6719
|
+
id: input.id,
|
|
6720
|
+
path: input.path,
|
|
6721
|
+
status: ok ? "ok" : "mismatch",
|
|
6722
|
+
expected_sha256: store.sha256,
|
|
6723
|
+
actual_sha256: actualSha,
|
|
6724
|
+
expected_size_bytes: store.size_bytes,
|
|
6725
|
+
actual_size_bytes: actualSize,
|
|
6726
|
+
relative_path: store.relative_path,
|
|
6727
|
+
message: ok ? "stored artifact content matches metadata" : "stored artifact content does not match metadata"
|
|
6728
|
+
};
|
|
6729
|
+
}
|
|
6730
|
+
function exportStoredArtifactContent(input) {
|
|
6731
|
+
const report = verifyStoredArtifact(input);
|
|
6732
|
+
if (report.status !== "ok" || !report.relative_path || !report.actual_sha256 || report.actual_size_bytes === null)
|
|
6733
|
+
return null;
|
|
6734
|
+
const content = readFileSync2(artifactStorePath(report.relative_path));
|
|
6735
|
+
return {
|
|
6736
|
+
artifact_id: input.id,
|
|
6737
|
+
sha256: report.actual_sha256,
|
|
6738
|
+
size_bytes: report.actual_size_bytes,
|
|
6739
|
+
relative_path: report.relative_path,
|
|
6740
|
+
base64: content.toString("base64")
|
|
6741
|
+
};
|
|
6742
|
+
}
|
|
6743
|
+
function importStoredArtifactContent(content) {
|
|
6744
|
+
const buffer = Buffer.from(content.base64, "base64");
|
|
6745
|
+
const actualSha = sha256(buffer);
|
|
6746
|
+
if (actualSha !== content.sha256 || buffer.length !== content.size_bytes) {
|
|
6747
|
+
return {
|
|
6748
|
+
id: content.artifact_id,
|
|
6749
|
+
path: content.relative_path,
|
|
6750
|
+
status: "mismatch",
|
|
6751
|
+
expected_sha256: content.sha256,
|
|
6752
|
+
actual_sha256: actualSha,
|
|
6753
|
+
expected_size_bytes: content.size_bytes,
|
|
6754
|
+
actual_size_bytes: buffer.length,
|
|
6755
|
+
relative_path: content.relative_path,
|
|
6756
|
+
message: "exported artifact content checksum does not match manifest"
|
|
6757
|
+
};
|
|
6758
|
+
}
|
|
6759
|
+
const destination = artifactStorePath(content.relative_path);
|
|
6760
|
+
mkdirSync4(dirname4(destination), { recursive: true });
|
|
6761
|
+
writeFileSync2(destination, buffer);
|
|
6762
|
+
return {
|
|
6763
|
+
id: content.artifact_id,
|
|
6764
|
+
path: content.relative_path,
|
|
6765
|
+
status: "ok",
|
|
6766
|
+
expected_sha256: content.sha256,
|
|
6767
|
+
actual_sha256: actualSha,
|
|
6768
|
+
expected_size_bytes: content.size_bytes,
|
|
6769
|
+
actual_size_bytes: buffer.length,
|
|
6770
|
+
relative_path: content.relative_path,
|
|
6771
|
+
message: "stored artifact content imported"
|
|
6772
|
+
};
|
|
6773
|
+
}
|
|
6774
|
+
|
|
6775
|
+
// src/db/task-runs.ts
|
|
6776
|
+
init_types();
|
|
6777
|
+
|
|
6778
|
+
// src/db/comments.ts
|
|
6779
|
+
init_types();
|
|
6780
|
+
init_database();
|
|
6781
|
+
function addComment(input, db) {
|
|
6782
|
+
const d = db || getDatabase();
|
|
6783
|
+
if (!getTask(input.task_id, d)) {
|
|
6784
|
+
throw new TaskNotFoundError(input.task_id);
|
|
6785
|
+
}
|
|
6786
|
+
const id = uuid();
|
|
6787
|
+
const timestamp = now();
|
|
6788
|
+
d.run(`INSERT INTO task_comments (id, task_id, agent_id, session_id, content, type, progress_pct, created_at)
|
|
6789
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
6790
|
+
id,
|
|
6791
|
+
input.task_id,
|
|
6792
|
+
input.agent_id || null,
|
|
6793
|
+
input.session_id || null,
|
|
6794
|
+
redactEvidenceText(input.content),
|
|
6795
|
+
input.type || "comment",
|
|
6796
|
+
input.progress_pct ?? null,
|
|
6797
|
+
timestamp
|
|
6798
|
+
]);
|
|
6799
|
+
return getComment(id, d);
|
|
6800
|
+
}
|
|
6801
|
+
function logProgress(taskId, message, pct, agentId, db) {
|
|
6802
|
+
return addComment({ task_id: taskId, content: message, type: "progress", progress_pct: pct, agent_id: agentId }, db);
|
|
6803
|
+
}
|
|
6804
|
+
function getComment(id, db) {
|
|
6805
|
+
const d = db || getDatabase();
|
|
6806
|
+
return d.query("SELECT * FROM task_comments WHERE id = ?").get(id);
|
|
6807
|
+
}
|
|
6808
|
+
function listComments(taskId, db) {
|
|
6809
|
+
const d = db || getDatabase();
|
|
6810
|
+
return d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(taskId);
|
|
6811
|
+
}
|
|
6812
|
+
function deleteComment(id, db) {
|
|
6813
|
+
const d = db || getDatabase();
|
|
6814
|
+
const result = d.run("DELETE FROM task_comments WHERE id = ?", [id]);
|
|
6815
|
+
return result.changes > 0;
|
|
6816
|
+
}
|
|
6817
|
+
|
|
6818
|
+
// src/db/task-runs.ts
|
|
6819
|
+
init_database();
|
|
6820
|
+
|
|
6821
|
+
// src/db/task-files.ts
|
|
6822
|
+
init_database();
|
|
6823
|
+
function addTaskFile(input, db) {
|
|
6824
|
+
const d = db || getDatabase();
|
|
6825
|
+
const id = uuid();
|
|
6826
|
+
const timestamp = now();
|
|
6827
|
+
const existing = d.query("SELECT id FROM task_files WHERE task_id = ? AND path = ?").get(input.task_id, input.path);
|
|
6828
|
+
if (existing) {
|
|
6829
|
+
d.run("UPDATE task_files SET status = ?, agent_id = ?, note = ?, updated_at = ? WHERE id = ?", [input.status || "active", input.agent_id || null, input.note || null, timestamp, existing.id]);
|
|
6830
|
+
return getTaskFile(existing.id, d);
|
|
6831
|
+
}
|
|
6832
|
+
d.run(`INSERT INTO task_files (id, task_id, path, status, agent_id, note, created_at, updated_at)
|
|
6833
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.task_id, input.path, input.status || "active", input.agent_id || null, input.note || null, timestamp, timestamp]);
|
|
6834
|
+
return getTaskFile(id, d);
|
|
6835
|
+
}
|
|
6836
|
+
function getTaskFile(id, db) {
|
|
6837
|
+
const d = db || getDatabase();
|
|
6838
|
+
return d.query("SELECT * FROM task_files WHERE id = ?").get(id);
|
|
6839
|
+
}
|
|
6840
|
+
function listTaskFiles(taskId, db) {
|
|
6841
|
+
const d = db || getDatabase();
|
|
6842
|
+
return d.query("SELECT * FROM task_files WHERE task_id = ? ORDER BY path").all(taskId);
|
|
6843
|
+
}
|
|
6844
|
+
function findTasksByFile(path, db) {
|
|
6845
|
+
const d = db || getDatabase();
|
|
6846
|
+
return d.query("SELECT * FROM task_files WHERE path = ? AND status != 'removed' ORDER BY updated_at DESC").all(path);
|
|
6847
|
+
}
|
|
6848
|
+
function updateTaskFileStatus(taskId, path, status, agentId, db) {
|
|
6849
|
+
const d = db || getDatabase();
|
|
6850
|
+
const timestamp = now();
|
|
6851
|
+
d.run("UPDATE task_files SET status = ?, agent_id = COALESCE(?, agent_id), updated_at = ? WHERE task_id = ? AND path = ?", [status, agentId || null, timestamp, taskId, path]);
|
|
6852
|
+
const row = d.query("SELECT * FROM task_files WHERE task_id = ? AND path = ?").get(taskId, path);
|
|
6853
|
+
return row;
|
|
6854
|
+
}
|
|
6855
|
+
function removeTaskFile(taskId, path, db) {
|
|
6856
|
+
const d = db || getDatabase();
|
|
6857
|
+
const result = d.run("DELETE FROM task_files WHERE task_id = ? AND path = ?", [taskId, path]);
|
|
6858
|
+
return result.changes > 0;
|
|
6859
|
+
}
|
|
6860
|
+
function bulkAddTaskFiles(taskId, paths, agentId, db) {
|
|
6861
|
+
const d = db || getDatabase();
|
|
6862
|
+
const results = [];
|
|
6863
|
+
const tx = d.transaction(() => {
|
|
6864
|
+
for (const path of paths) {
|
|
6865
|
+
results.push(addTaskFile({ task_id: taskId, path, agent_id: agentId }, d));
|
|
6866
|
+
}
|
|
6867
|
+
});
|
|
6868
|
+
tx();
|
|
6869
|
+
return results;
|
|
6870
|
+
}
|
|
6871
|
+
|
|
6872
|
+
// src/db/task-commits.ts
|
|
6873
|
+
init_database();
|
|
6874
|
+
function rowToCommit(row) {
|
|
6875
|
+
return {
|
|
6876
|
+
...row,
|
|
6877
|
+
files_changed: row.files_changed ? JSON.parse(row.files_changed) : null
|
|
6878
|
+
};
|
|
6879
|
+
}
|
|
6880
|
+
function rowToGitRef(row) {
|
|
6881
|
+
return {
|
|
6882
|
+
...row,
|
|
6883
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : {}
|
|
6884
|
+
};
|
|
6885
|
+
}
|
|
6886
|
+
function rowToVerification(row) {
|
|
6887
|
+
return row;
|
|
6888
|
+
}
|
|
6889
|
+
function getTaskCommits(taskId, db) {
|
|
6890
|
+
const d = db || getDatabase();
|
|
6891
|
+
return d.query("SELECT * FROM task_commits WHERE task_id = ? ORDER BY committed_at DESC, created_at DESC").all(taskId).map(rowToCommit);
|
|
6892
|
+
}
|
|
6893
|
+
function getTaskGitRefs(taskId, db) {
|
|
6894
|
+
const d = db || getDatabase();
|
|
6895
|
+
return d.query("SELECT * FROM task_git_refs WHERE task_id = ? ORDER BY ref_type, updated_at DESC").all(taskId).map(rowToGitRef);
|
|
6896
|
+
}
|
|
6897
|
+
function addTaskVerification(input, db) {
|
|
6898
|
+
const d = db || getDatabase();
|
|
6899
|
+
const id = uuid();
|
|
6900
|
+
const runAt = input.run_at || now();
|
|
6901
|
+
d.run("INSERT INTO task_verifications (id, task_id, command, status, output_summary, artifact_path, agent_id, run_at, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", [
|
|
6902
|
+
id,
|
|
6903
|
+
input.task_id,
|
|
6904
|
+
input.command,
|
|
6905
|
+
input.status || "unknown",
|
|
6906
|
+
input.output_summary ?? null,
|
|
6907
|
+
input.artifact_path ?? null,
|
|
6908
|
+
input.agent_id ?? null,
|
|
6909
|
+
runAt,
|
|
6910
|
+
now()
|
|
6911
|
+
]);
|
|
6912
|
+
return rowToVerification(d.query("SELECT * FROM task_verifications WHERE id = ?").get(id));
|
|
5248
6913
|
}
|
|
5249
|
-
function
|
|
6914
|
+
function getTaskVerifications(taskId, db) {
|
|
5250
6915
|
const d = db || getDatabase();
|
|
5251
|
-
|
|
5252
|
-
return row;
|
|
6916
|
+
return d.query("SELECT * FROM task_verifications WHERE task_id = ? ORDER BY run_at DESC, created_at DESC").all(taskId).map(rowToVerification);
|
|
5253
6917
|
}
|
|
5254
|
-
function
|
|
6918
|
+
function getTaskTraceability(taskId, db) {
|
|
5255
6919
|
const d = db || getDatabase();
|
|
5256
|
-
|
|
5257
|
-
|
|
6920
|
+
return {
|
|
6921
|
+
task_id: taskId,
|
|
6922
|
+
commits: getTaskCommits(taskId, d),
|
|
6923
|
+
git_refs: getTaskGitRefs(taskId, d),
|
|
6924
|
+
verifications: getTaskVerifications(taskId, d)
|
|
6925
|
+
};
|
|
6926
|
+
}
|
|
6927
|
+
|
|
6928
|
+
// src/db/task-runs.ts
|
|
6929
|
+
function parseObject(value) {
|
|
6930
|
+
if (!value)
|
|
6931
|
+
return {};
|
|
6932
|
+
try {
|
|
6933
|
+
const parsed = JSON.parse(value);
|
|
6934
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
6935
|
+
} catch {
|
|
6936
|
+
return {};
|
|
5258
6937
|
}
|
|
5259
|
-
return d.query("SELECT * FROM plans ORDER BY created_at DESC").all();
|
|
5260
6938
|
}
|
|
5261
|
-
function
|
|
6939
|
+
function rowToRun(row) {
|
|
6940
|
+
return { ...row, metadata: parseObject(row.metadata) };
|
|
6941
|
+
}
|
|
6942
|
+
function rowToEvent(row) {
|
|
6943
|
+
return { ...row, data: parseObject(row.data) };
|
|
6944
|
+
}
|
|
6945
|
+
function rowToArtifact(row) {
|
|
6946
|
+
return { ...row, metadata: parseObject(row.metadata) };
|
|
6947
|
+
}
|
|
6948
|
+
function getRunRow(runId, db) {
|
|
6949
|
+
return db.query("SELECT * FROM task_runs WHERE id = ?").get(runId);
|
|
6950
|
+
}
|
|
6951
|
+
function resolveTaskRunId(idOrPrefix, db) {
|
|
5262
6952
|
const d = db || getDatabase();
|
|
5263
|
-
const
|
|
5264
|
-
if (
|
|
5265
|
-
throw new
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
6953
|
+
const rows = d.query("SELECT id FROM task_runs WHERE id = ? OR id LIKE ? ORDER BY created_at DESC LIMIT 2").all(idOrPrefix, `${idOrPrefix}%`);
|
|
6954
|
+
if (rows.length === 0)
|
|
6955
|
+
throw new Error(`Run not found: ${idOrPrefix}`);
|
|
6956
|
+
if (rows.length > 1)
|
|
6957
|
+
throw new Error(`Run ID is ambiguous: ${idOrPrefix}`);
|
|
6958
|
+
return rows[0].id;
|
|
6959
|
+
}
|
|
6960
|
+
function getTaskRun(runId, db) {
|
|
6961
|
+
const d = db || getDatabase();
|
|
6962
|
+
const row = getRunRow(runId, d);
|
|
6963
|
+
return row ? rowToRun(row) : null;
|
|
6964
|
+
}
|
|
6965
|
+
function startTaskRun(input, db) {
|
|
6966
|
+
const d = db || getDatabase();
|
|
6967
|
+
if (!getTask(input.task_id, d))
|
|
6968
|
+
throw new TaskNotFoundError(input.task_id);
|
|
6969
|
+
const id = uuid();
|
|
6970
|
+
const timestamp = input.started_at || now();
|
|
6971
|
+
if (input.claim && input.agent_id) {
|
|
6972
|
+
startTask(input.task_id, input.agent_id, d);
|
|
5279
6973
|
}
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
6974
|
+
d.run("INSERT INTO task_runs (id, task_id, agent_id, title, status, summary, metadata, started_at, created_at, updated_at) VALUES (?, ?, ?, ?, 'running', ?, ?, ?, ?, ?)", [
|
|
6975
|
+
id,
|
|
6976
|
+
input.task_id,
|
|
6977
|
+
input.agent_id ?? null,
|
|
6978
|
+
input.title ? redactEvidenceText(input.title) : null,
|
|
6979
|
+
input.summary ? redactEvidenceText(input.summary) : null,
|
|
6980
|
+
JSON.stringify(redactValue(input.metadata || {})),
|
|
6981
|
+
timestamp,
|
|
6982
|
+
timestamp,
|
|
6983
|
+
timestamp
|
|
6984
|
+
]);
|
|
6985
|
+
addTaskRunEvent({
|
|
6986
|
+
run_id: id,
|
|
6987
|
+
event_type: "started",
|
|
6988
|
+
message: input.summary || input.title || "run started",
|
|
6989
|
+
data: { title: input.title, claim: Boolean(input.claim) },
|
|
6990
|
+
agent_id: input.agent_id,
|
|
6991
|
+
created_at: timestamp
|
|
6992
|
+
}, d);
|
|
6993
|
+
if (input.claim && input.agent_id) {
|
|
6994
|
+
addTaskRunEvent({
|
|
6995
|
+
run_id: id,
|
|
6996
|
+
event_type: "claim",
|
|
6997
|
+
message: `claimed by ${input.agent_id}`,
|
|
6998
|
+
data: { agent_id: input.agent_id },
|
|
6999
|
+
agent_id: input.agent_id,
|
|
7000
|
+
created_at: timestamp
|
|
7001
|
+
}, d);
|
|
5283
7002
|
}
|
|
5284
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
7003
|
+
const run = getTaskRun(id, d);
|
|
7004
|
+
emitLocalEventHooksQuiet({ type: "run.started", payload: { id: run.id, task_id: run.task_id, agent_id: run.agent_id, title: run.title } });
|
|
7005
|
+
return run;
|
|
7006
|
+
}
|
|
7007
|
+
function addTaskRunEvent(input, db) {
|
|
7008
|
+
const d = db || getDatabase();
|
|
7009
|
+
const runId = resolveTaskRunId(input.run_id, d);
|
|
7010
|
+
const run = getTaskRun(runId, d);
|
|
7011
|
+
if (!run)
|
|
7012
|
+
throw new Error(`Run not found: ${input.run_id}`);
|
|
7013
|
+
const id = uuid();
|
|
7014
|
+
const timestamp = input.created_at || now();
|
|
7015
|
+
d.run("INSERT INTO task_run_events (id, run_id, task_id, event_type, message, data, agent_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [
|
|
7016
|
+
id,
|
|
7017
|
+
run.id,
|
|
7018
|
+
run.task_id,
|
|
7019
|
+
input.event_type,
|
|
7020
|
+
input.message ? redactEvidenceText(input.message) : null,
|
|
7021
|
+
JSON.stringify(redactValue(input.data || {})),
|
|
7022
|
+
input.agent_id ?? run.agent_id,
|
|
7023
|
+
timestamp
|
|
7024
|
+
]);
|
|
7025
|
+
if (input.event_type === "comment" && input.message) {
|
|
7026
|
+
addComment({ task_id: run.task_id, content: redactEvidenceText(input.message), type: "comment", agent_id: input.agent_id ?? run.agent_id ?? undefined }, d);
|
|
5287
7027
|
}
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
7028
|
+
return rowToEvent(d.query("SELECT * FROM task_run_events WHERE id = ?").get(id));
|
|
7029
|
+
}
|
|
7030
|
+
function addTaskRunCommand(input, db) {
|
|
7031
|
+
const d = db || getDatabase();
|
|
7032
|
+
const runId = resolveTaskRunId(input.run_id, d);
|
|
7033
|
+
const run = getTaskRun(runId, d);
|
|
7034
|
+
if (!run)
|
|
7035
|
+
throw new Error(`Run not found: ${input.run_id}`);
|
|
7036
|
+
const id = uuid();
|
|
7037
|
+
const status = input.status || "unknown";
|
|
7038
|
+
const timestamp = now();
|
|
7039
|
+
const command = redactEvidenceText(input.command);
|
|
7040
|
+
const outputSummary = input.output_summary ? redactEvidenceText(input.output_summary) : null;
|
|
7041
|
+
const artifactPath = input.artifact_path ? redactEvidenceText(input.artifact_path) : null;
|
|
7042
|
+
d.run("INSERT INTO task_run_commands (id, run_id, task_id, command, status, exit_code, output_summary, artifact_path, agent_id, started_at, completed_at, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
|
|
7043
|
+
id,
|
|
7044
|
+
run.id,
|
|
7045
|
+
run.task_id,
|
|
7046
|
+
command,
|
|
7047
|
+
status,
|
|
7048
|
+
input.exit_code ?? null,
|
|
7049
|
+
outputSummary,
|
|
7050
|
+
artifactPath,
|
|
7051
|
+
input.agent_id ?? run.agent_id,
|
|
7052
|
+
input.started_at ?? null,
|
|
7053
|
+
input.completed_at ?? timestamp,
|
|
7054
|
+
timestamp
|
|
7055
|
+
]);
|
|
7056
|
+
addTaskVerification({
|
|
7057
|
+
task_id: run.task_id,
|
|
7058
|
+
command,
|
|
7059
|
+
status,
|
|
7060
|
+
output_summary: outputSummary ?? undefined,
|
|
7061
|
+
artifact_path: artifactPath ?? undefined,
|
|
7062
|
+
agent_id: input.agent_id ?? run.agent_id ?? undefined,
|
|
7063
|
+
run_at: input.completed_at ?? timestamp
|
|
7064
|
+
}, d);
|
|
7065
|
+
addTaskRunEvent({
|
|
7066
|
+
run_id: run.id,
|
|
7067
|
+
event_type: "command",
|
|
7068
|
+
message: `${status}: ${command}`,
|
|
7069
|
+
data: { command, status, exit_code: input.exit_code ?? null, output_summary: outputSummary, artifact_path: artifactPath },
|
|
7070
|
+
agent_id: input.agent_id ?? run.agent_id ?? undefined,
|
|
7071
|
+
created_at: timestamp
|
|
7072
|
+
}, d);
|
|
7073
|
+
return d.query("SELECT * FROM task_run_commands WHERE id = ?").get(id);
|
|
7074
|
+
}
|
|
7075
|
+
function addTaskRunFile(input, db) {
|
|
7076
|
+
const d = db || getDatabase();
|
|
7077
|
+
const runId = resolveTaskRunId(input.run_id, d);
|
|
7078
|
+
const run = getTaskRun(runId, d);
|
|
7079
|
+
if (!run)
|
|
7080
|
+
throw new Error(`Run not found: ${input.run_id}`);
|
|
7081
|
+
const file = addTaskFile({
|
|
7082
|
+
task_id: run.task_id,
|
|
7083
|
+
path: input.path,
|
|
7084
|
+
status: input.status || "modified",
|
|
7085
|
+
note: input.note ? redactEvidenceText(input.note) : undefined,
|
|
7086
|
+
agent_id: input.agent_id ?? run.agent_id ?? undefined
|
|
7087
|
+
}, d);
|
|
7088
|
+
addTaskRunEvent({
|
|
7089
|
+
run_id: run.id,
|
|
7090
|
+
event_type: "file",
|
|
7091
|
+
message: `${file.status}: ${file.path}`,
|
|
7092
|
+
data: { path: file.path, status: file.status, note: file.note },
|
|
7093
|
+
agent_id: input.agent_id ?? run.agent_id ?? undefined
|
|
7094
|
+
}, d);
|
|
7095
|
+
return file;
|
|
7096
|
+
}
|
|
7097
|
+
function addTaskRunArtifact(input, db) {
|
|
7098
|
+
const d = db || getDatabase();
|
|
7099
|
+
const runId = resolveTaskRunId(input.run_id, d);
|
|
7100
|
+
const run = getTaskRun(runId, d);
|
|
7101
|
+
if (!run)
|
|
7102
|
+
throw new Error(`Run not found: ${input.run_id}`);
|
|
7103
|
+
const id = uuid();
|
|
7104
|
+
const timestamp = now();
|
|
7105
|
+
const path = redactEvidenceText(input.path);
|
|
7106
|
+
const description = input.description ? redactEvidenceText(input.description) : null;
|
|
7107
|
+
const metadata = redactValue(input.metadata || {});
|
|
7108
|
+
let sizeBytes = input.size_bytes ?? null;
|
|
7109
|
+
let digest = input.sha256 ?? null;
|
|
7110
|
+
const stored = input.store_content !== false ? storeArtifactContent({ path: input.path, metadata, retention_days: input.retention_days, created_at: timestamp }) : null;
|
|
7111
|
+
if (stored) {
|
|
7112
|
+
sizeBytes = stored.size_bytes;
|
|
7113
|
+
digest = stored.sha256;
|
|
7114
|
+
metadata["artifact_store"] = stored.store;
|
|
7115
|
+
} else if (input.store_content === true) {
|
|
7116
|
+
throw new Error(`Artifact file not found: ${input.path}`);
|
|
7117
|
+
}
|
|
7118
|
+
d.run("INSERT INTO task_run_artifacts (id, run_id, task_id, path, artifact_type, description, size_bytes, sha256, metadata, agent_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
|
|
7119
|
+
id,
|
|
7120
|
+
run.id,
|
|
7121
|
+
run.task_id,
|
|
7122
|
+
path,
|
|
7123
|
+
input.artifact_type ?? null,
|
|
7124
|
+
description,
|
|
7125
|
+
sizeBytes,
|
|
7126
|
+
digest,
|
|
7127
|
+
JSON.stringify(metadata),
|
|
7128
|
+
input.agent_id ?? run.agent_id,
|
|
7129
|
+
timestamp
|
|
7130
|
+
]);
|
|
7131
|
+
addTaskRunEvent({
|
|
7132
|
+
run_id: run.id,
|
|
7133
|
+
event_type: "artifact",
|
|
7134
|
+
message: description || path,
|
|
7135
|
+
data: { path, artifact_type: input.artifact_type, size_bytes: sizeBytes, sha256: digest, stored: Boolean(stored) },
|
|
7136
|
+
agent_id: input.agent_id ?? run.agent_id ?? undefined,
|
|
7137
|
+
created_at: timestamp
|
|
7138
|
+
}, d);
|
|
7139
|
+
return rowToArtifact(d.query("SELECT * FROM task_run_artifacts WHERE id = ?").get(id));
|
|
7140
|
+
}
|
|
7141
|
+
function verifyTaskRunArtifacts(runId, db) {
|
|
7142
|
+
const ledger = getTaskRunLedger(runId, db);
|
|
7143
|
+
return ledger.artifacts.map((artifact) => verifyStoredArtifact({
|
|
7144
|
+
id: artifact.id,
|
|
7145
|
+
path: artifact.path,
|
|
7146
|
+
size_bytes: artifact.size_bytes,
|
|
7147
|
+
sha256: artifact.sha256,
|
|
7148
|
+
metadata: artifact.metadata
|
|
7149
|
+
}));
|
|
7150
|
+
}
|
|
7151
|
+
function finishTaskRun(input, db) {
|
|
7152
|
+
const d = db || getDatabase();
|
|
7153
|
+
const runId = resolveTaskRunId(input.run_id, d);
|
|
7154
|
+
const run = getTaskRun(runId, d);
|
|
7155
|
+
if (!run)
|
|
7156
|
+
throw new Error(`Run not found: ${input.run_id}`);
|
|
7157
|
+
const timestamp = input.completed_at || now();
|
|
7158
|
+
const summary = input.summary ? redactEvidenceText(input.summary) : null;
|
|
7159
|
+
d.run("UPDATE task_runs SET status = ?, summary = COALESCE(?, summary), completed_at = ?, updated_at = ? WHERE id = ?", [input.status, summary, timestamp, timestamp, run.id]);
|
|
7160
|
+
addTaskRunEvent({
|
|
7161
|
+
run_id: run.id,
|
|
7162
|
+
event_type: input.status,
|
|
7163
|
+
message: summary || `run ${input.status}`,
|
|
7164
|
+
data: { status: input.status },
|
|
7165
|
+
agent_id: input.agent_id ?? run.agent_id ?? undefined,
|
|
7166
|
+
created_at: timestamp
|
|
7167
|
+
}, d);
|
|
7168
|
+
const updated = getTaskRun(run.id, d);
|
|
5291
7169
|
emitLocalEventHooksQuiet({
|
|
5292
|
-
type:
|
|
5293
|
-
payload: { id,
|
|
7170
|
+
type: `run.${input.status}`,
|
|
7171
|
+
payload: { id: updated.id, task_id: updated.task_id, agent_id: updated.agent_id, status: updated.status, summary: updated.summary, completed_at: timestamp }
|
|
5294
7172
|
});
|
|
5295
7173
|
return updated;
|
|
5296
7174
|
}
|
|
5297
|
-
function
|
|
7175
|
+
function listTaskRuns(taskId, db) {
|
|
5298
7176
|
const d = db || getDatabase();
|
|
5299
|
-
const
|
|
5300
|
-
return
|
|
7177
|
+
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();
|
|
7178
|
+
return rows.map(rowToRun);
|
|
7179
|
+
}
|
|
7180
|
+
function getTaskRunLedger(runId, db) {
|
|
7181
|
+
const d = db || getDatabase();
|
|
7182
|
+
const resolved = resolveTaskRunId(runId, d);
|
|
7183
|
+
const run = getTaskRun(resolved, d);
|
|
7184
|
+
if (!run)
|
|
7185
|
+
throw new Error(`Run not found: ${runId}`);
|
|
7186
|
+
const events = d.query("SELECT * FROM task_run_events WHERE run_id = ? ORDER BY created_at, rowid").all(run.id).map(rowToEvent);
|
|
7187
|
+
const commands = d.query("SELECT * FROM task_run_commands WHERE run_id = ? ORDER BY created_at, rowid").all(run.id);
|
|
7188
|
+
const artifacts = d.query("SELECT * FROM task_run_artifacts WHERE run_id = ? ORDER BY created_at, rowid").all(run.id).map(rowToArtifact);
|
|
7189
|
+
const files = d.query("SELECT * FROM task_files WHERE task_id = ? ORDER BY updated_at DESC, path").all(run.task_id);
|
|
7190
|
+
return { run, events, commands, artifacts, files };
|
|
5301
7191
|
}
|
|
5302
7192
|
|
|
7193
|
+
// src/db/calendar.ts
|
|
7194
|
+
function parseJsonObject2(value) {
|
|
7195
|
+
if (!value)
|
|
7196
|
+
return {};
|
|
7197
|
+
if (typeof value === "object" && !Array.isArray(value))
|
|
7198
|
+
return value;
|
|
7199
|
+
if (typeof value !== "string")
|
|
7200
|
+
return {};
|
|
7201
|
+
try {
|
|
7202
|
+
const parsed = JSON.parse(value);
|
|
7203
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
7204
|
+
} catch {
|
|
7205
|
+
return {};
|
|
7206
|
+
}
|
|
7207
|
+
}
|
|
7208
|
+
function rowToCalendarItem(row) {
|
|
7209
|
+
return { ...row, metadata: parseJsonObject2(row.metadata) };
|
|
7210
|
+
}
|
|
7211
|
+
function eventFromLocal(item) {
|
|
7212
|
+
return {
|
|
7213
|
+
...item,
|
|
7214
|
+
source: "local",
|
|
7215
|
+
badges: [item.kind, item.timezone || "floating"]
|
|
7216
|
+
};
|
|
7217
|
+
}
|
|
7218
|
+
function addMinutes(iso, minutes) {
|
|
7219
|
+
const ms = Date.parse(iso);
|
|
7220
|
+
if (!Number.isFinite(ms))
|
|
7221
|
+
return iso;
|
|
7222
|
+
return new Date(ms + minutes * 60000).toISOString();
|
|
7223
|
+
}
|
|
7224
|
+
function eventFromTaskDue(task) {
|
|
7225
|
+
if (!task.due_at)
|
|
7226
|
+
return null;
|
|
7227
|
+
return {
|
|
7228
|
+
id: `task-due-${task.id}`,
|
|
7229
|
+
kind: "task_due",
|
|
7230
|
+
title: `Due: ${task.title}`,
|
|
7231
|
+
description: task.description,
|
|
7232
|
+
starts_at: task.due_at,
|
|
7233
|
+
ends_at: null,
|
|
7234
|
+
timezone: null,
|
|
7235
|
+
project_id: task.project_id,
|
|
7236
|
+
task_id: task.id,
|
|
7237
|
+
plan_id: task.plan_id,
|
|
7238
|
+
run_id: null,
|
|
7239
|
+
recurrence_rule: task.recurrence_rule,
|
|
7240
|
+
source: "task",
|
|
7241
|
+
badges: ["due", task.priority, task.status],
|
|
7242
|
+
metadata: { priority: task.priority, status: task.status, short_id: task.short_id }
|
|
7243
|
+
};
|
|
7244
|
+
}
|
|
7245
|
+
function eventFromTaskSla(task) {
|
|
7246
|
+
if (!task.sla_minutes || ["completed", "cancelled", "failed"].includes(task.status))
|
|
7247
|
+
return null;
|
|
7248
|
+
const base = task.started_at || task.created_at;
|
|
7249
|
+
if (!base)
|
|
7250
|
+
return null;
|
|
7251
|
+
const startsAt = addMinutes(base, task.sla_minutes);
|
|
7252
|
+
return {
|
|
7253
|
+
id: `task-sla-${task.id}`,
|
|
7254
|
+
kind: "task_sla",
|
|
7255
|
+
title: `SLA: ${task.title}`,
|
|
7256
|
+
description: task.description,
|
|
7257
|
+
starts_at: startsAt,
|
|
7258
|
+
ends_at: null,
|
|
7259
|
+
timezone: null,
|
|
7260
|
+
project_id: task.project_id,
|
|
7261
|
+
task_id: task.id,
|
|
7262
|
+
plan_id: task.plan_id,
|
|
7263
|
+
run_id: null,
|
|
7264
|
+
recurrence_rule: null,
|
|
7265
|
+
source: "task",
|
|
7266
|
+
badges: ["sla", task.priority, task.status],
|
|
7267
|
+
metadata: { sla_minutes: task.sla_minutes, status: task.status, short_id: task.short_id }
|
|
7268
|
+
};
|
|
7269
|
+
}
|
|
7270
|
+
function eventFromRun(run, task) {
|
|
7271
|
+
return {
|
|
7272
|
+
id: `run-${run.id}`,
|
|
7273
|
+
kind: "run",
|
|
7274
|
+
title: `Run: ${run.title || task?.title || run.id.slice(0, 8)}`,
|
|
7275
|
+
description: run.summary,
|
|
7276
|
+
starts_at: run.started_at,
|
|
7277
|
+
ends_at: run.completed_at,
|
|
7278
|
+
timezone: null,
|
|
7279
|
+
project_id: task?.project_id || null,
|
|
7280
|
+
task_id: run.task_id,
|
|
7281
|
+
plan_id: task?.plan_id || null,
|
|
7282
|
+
run_id: run.id,
|
|
7283
|
+
recurrence_rule: null,
|
|
7284
|
+
source: "run",
|
|
7285
|
+
badges: ["run", run.status],
|
|
7286
|
+
metadata: { status: run.status, agent_id: run.agent_id }
|
|
7287
|
+
};
|
|
7288
|
+
}
|
|
7289
|
+
function inWindow(event, query) {
|
|
7290
|
+
const start = Date.parse(event.starts_at);
|
|
7291
|
+
if (query.from && Number.isFinite(start) && start < Date.parse(query.from))
|
|
7292
|
+
return false;
|
|
7293
|
+
if (query.to && Number.isFinite(start) && start > Date.parse(query.to))
|
|
7294
|
+
return false;
|
|
7295
|
+
if (query.kind && event.kind !== query.kind)
|
|
7296
|
+
return false;
|
|
7297
|
+
if (query.project_id && event.project_id !== query.project_id)
|
|
7298
|
+
return false;
|
|
7299
|
+
if (query.task_id && event.task_id !== query.task_id)
|
|
7300
|
+
return false;
|
|
7301
|
+
if (query.plan_id && event.plan_id !== query.plan_id)
|
|
7302
|
+
return false;
|
|
7303
|
+
if (query.run_id && event.run_id !== query.run_id)
|
|
7304
|
+
return false;
|
|
7305
|
+
return true;
|
|
7306
|
+
}
|
|
7307
|
+
function createCalendarItem(input, db) {
|
|
7308
|
+
const d = db || getDatabase();
|
|
7309
|
+
const id = uuid();
|
|
7310
|
+
const timestamp = now();
|
|
7311
|
+
const kind = input.kind || "work_block";
|
|
7312
|
+
d.run(`INSERT INTO local_calendar_items (
|
|
7313
|
+
id, kind, title, description, starts_at, ends_at, timezone, project_id, task_id, plan_id, run_id,
|
|
7314
|
+
recurrence_rule, metadata, created_at, updated_at
|
|
7315
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
7316
|
+
id,
|
|
7317
|
+
kind,
|
|
7318
|
+
input.title,
|
|
7319
|
+
input.description || null,
|
|
7320
|
+
input.starts_at,
|
|
7321
|
+
input.ends_at || null,
|
|
7322
|
+
input.timezone || null,
|
|
7323
|
+
input.project_id || null,
|
|
7324
|
+
input.task_id || null,
|
|
7325
|
+
input.plan_id || null,
|
|
7326
|
+
input.run_id || null,
|
|
7327
|
+
input.recurrence_rule || null,
|
|
7328
|
+
JSON.stringify(input.metadata || {}),
|
|
7329
|
+
timestamp,
|
|
7330
|
+
timestamp
|
|
7331
|
+
]);
|
|
7332
|
+
return getCalendarItem(id, d);
|
|
7333
|
+
}
|
|
7334
|
+
function getCalendarItem(id, db) {
|
|
7335
|
+
const d = db || getDatabase();
|
|
7336
|
+
const resolved = id.length >= 36 ? id : d.query("SELECT id FROM local_calendar_items WHERE id LIKE ?").all(`${id}%`).length === 1 ? d.query("SELECT id FROM local_calendar_items WHERE id LIKE ?").get(`${id}%`).id : id;
|
|
7337
|
+
const row = d.query("SELECT * FROM local_calendar_items WHERE id = ?").get(resolved);
|
|
7338
|
+
return row ? rowToCalendarItem(row) : null;
|
|
7339
|
+
}
|
|
7340
|
+
function listCalendarItems(query = {}, db) {
|
|
7341
|
+
const d = db || getDatabase();
|
|
7342
|
+
const conditions = [];
|
|
7343
|
+
const params = [];
|
|
7344
|
+
if (query.project_id) {
|
|
7345
|
+
conditions.push("project_id = ?");
|
|
7346
|
+
params.push(query.project_id);
|
|
7347
|
+
}
|
|
7348
|
+
if (query.task_id) {
|
|
7349
|
+
conditions.push("task_id = ?");
|
|
7350
|
+
params.push(query.task_id);
|
|
7351
|
+
}
|
|
7352
|
+
if (query.plan_id) {
|
|
7353
|
+
conditions.push("plan_id = ?");
|
|
7354
|
+
params.push(query.plan_id);
|
|
7355
|
+
}
|
|
7356
|
+
if (query.run_id) {
|
|
7357
|
+
conditions.push("run_id = ?");
|
|
7358
|
+
params.push(query.run_id);
|
|
7359
|
+
}
|
|
7360
|
+
if (query.kind) {
|
|
7361
|
+
conditions.push("kind = ?");
|
|
7362
|
+
params.push(query.kind);
|
|
7363
|
+
}
|
|
7364
|
+
if (query.from) {
|
|
7365
|
+
conditions.push("starts_at >= ?");
|
|
7366
|
+
params.push(query.from);
|
|
7367
|
+
}
|
|
7368
|
+
if (query.to) {
|
|
7369
|
+
conditions.push("starts_at <= ?");
|
|
7370
|
+
params.push(query.to);
|
|
7371
|
+
}
|
|
7372
|
+
let sql = "SELECT * FROM local_calendar_items";
|
|
7373
|
+
if (conditions.length > 0)
|
|
7374
|
+
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
7375
|
+
sql += " ORDER BY starts_at ASC, created_at ASC";
|
|
7376
|
+
if (query.limit) {
|
|
7377
|
+
sql += " LIMIT ?";
|
|
7378
|
+
params.push(query.limit);
|
|
7379
|
+
}
|
|
7380
|
+
return d.query(sql).all(...params).map(rowToCalendarItem);
|
|
7381
|
+
}
|
|
7382
|
+
function listCalendarEvents(query = {}, db) {
|
|
7383
|
+
const d = db || getDatabase();
|
|
7384
|
+
const taskFilter = {
|
|
7385
|
+
project_id: query.project_id,
|
|
7386
|
+
plan_id: query.plan_id,
|
|
7387
|
+
ids: query.task_id ? [query.task_id] : undefined,
|
|
7388
|
+
include_archived: true
|
|
7389
|
+
};
|
|
7390
|
+
const tasks = listTasks(taskFilter, d).filter((task) => query.include_completed || !["completed", "cancelled"].includes(task.status));
|
|
7391
|
+
const events = [];
|
|
7392
|
+
for (const task of tasks) {
|
|
7393
|
+
const due = eventFromTaskDue(task);
|
|
7394
|
+
if (due)
|
|
7395
|
+
events.push(due);
|
|
7396
|
+
if (query.include_sla !== false) {
|
|
7397
|
+
const sla = eventFromTaskSla(task);
|
|
7398
|
+
if (sla)
|
|
7399
|
+
events.push(sla);
|
|
7400
|
+
}
|
|
7401
|
+
}
|
|
7402
|
+
if (query.include_runs !== false) {
|
|
7403
|
+
const runs = query.task_id ? listTaskRuns(query.task_id, d) : listTaskRuns(undefined, d);
|
|
7404
|
+
for (const run of runs) {
|
|
7405
|
+
const task = getTask(run.task_id, d);
|
|
7406
|
+
events.push(eventFromRun(run, task));
|
|
7407
|
+
}
|
|
7408
|
+
}
|
|
7409
|
+
if (query.include_local !== false) {
|
|
7410
|
+
events.push(...listCalendarItems(query, d).map(eventFromLocal));
|
|
7411
|
+
}
|
|
7412
|
+
return events.filter((event) => inWindow(event, query)).sort((left, right) => left.starts_at.localeCompare(right.starts_at) || left.id.localeCompare(right.id)).slice(0, query.limit || undefined);
|
|
7413
|
+
}
|
|
7414
|
+
function pad(value) {
|
|
7415
|
+
return value < 10 ? `0${value}` : String(value);
|
|
7416
|
+
}
|
|
7417
|
+
function icsDate(iso) {
|
|
7418
|
+
const date = new Date(iso);
|
|
7419
|
+
if (Number.isNaN(date.getTime()))
|
|
7420
|
+
return iso.replace(/[-:]/g, "").replace(/\.\d{3}/, "");
|
|
7421
|
+
return `${date.getUTCFullYear()}${pad(date.getUTCMonth() + 1)}${pad(date.getUTCDate())}T${pad(date.getUTCHours())}${pad(date.getUTCMinutes())}${pad(date.getUTCSeconds())}Z`;
|
|
7422
|
+
}
|
|
7423
|
+
function escapeIcs(value) {
|
|
7424
|
+
return String(value || "").replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/,/g, "\\,").replace(/;/g, "\\;");
|
|
7425
|
+
}
|
|
7426
|
+
function foldLine(line) {
|
|
7427
|
+
if (line.length <= 75)
|
|
7428
|
+
return line;
|
|
7429
|
+
const chunks = [];
|
|
7430
|
+
let rest = line;
|
|
7431
|
+
while (rest.length > 75) {
|
|
7432
|
+
chunks.push(rest.slice(0, 75));
|
|
7433
|
+
rest = rest.slice(75);
|
|
7434
|
+
}
|
|
7435
|
+
chunks.push(rest);
|
|
7436
|
+
return chunks.map((chunk, index) => index === 0 ? chunk : ` ${chunk}`).join(`\r
|
|
7437
|
+
`);
|
|
7438
|
+
}
|
|
7439
|
+
function recurrenceToRrule(rule) {
|
|
7440
|
+
if (!rule)
|
|
7441
|
+
return null;
|
|
7442
|
+
const normalized = rule.trim().toLowerCase();
|
|
7443
|
+
if (normalized === "daily" || normalized === "every day")
|
|
7444
|
+
return "FREQ=DAILY";
|
|
7445
|
+
if (normalized === "weekly" || normalized === "every week")
|
|
7446
|
+
return "FREQ=WEEKLY";
|
|
7447
|
+
if (normalized === "monthly" || normalized === "every month")
|
|
7448
|
+
return "FREQ=MONTHLY";
|
|
7449
|
+
if (normalized === "every weekday")
|
|
7450
|
+
return "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR";
|
|
7451
|
+
const everyN = normalized.match(/^every (\d+) (day|days|week|weeks|month|months)$/);
|
|
7452
|
+
if (everyN) {
|
|
7453
|
+
const freq = everyN[2].startsWith("day") ? "DAILY" : everyN[2].startsWith("week") ? "WEEKLY" : "MONTHLY";
|
|
7454
|
+
return `FREQ=${freq};INTERVAL=${everyN[1]}`;
|
|
7455
|
+
}
|
|
7456
|
+
const days = normalized.match(/^every (mon|monday|tue|tuesday|wed|wednesday|thu|thursday|fri|friday|sat|saturday|sun|sunday)(,(mon|monday|tue|tuesday|wed|wednesday|thu|thursday|fri|friday|sat|saturday|sun|sunday))*$/);
|
|
7457
|
+
if (days) {
|
|
7458
|
+
const map = { mon: "MO", monday: "MO", tue: "TU", tuesday: "TU", wed: "WE", wednesday: "WE", thu: "TH", thursday: "TH", fri: "FR", friday: "FR", sat: "SA", saturday: "SA", sun: "SU", sunday: "SU" };
|
|
7459
|
+
const byday = normalized.replace("every ", "").split(",").map((day) => map[day] || "").filter(Boolean).join(",");
|
|
7460
|
+
if (byday)
|
|
7461
|
+
return `FREQ=WEEKLY;BYDAY=${byday}`;
|
|
7462
|
+
}
|
|
7463
|
+
if (normalized.startsWith("freq="))
|
|
7464
|
+
return rule.toUpperCase();
|
|
7465
|
+
return null;
|
|
7466
|
+
}
|
|
7467
|
+
function exportCalendarIcs(options = {}, db) {
|
|
7468
|
+
const events = listCalendarEvents(options, db);
|
|
7469
|
+
const generatedAt = options.generated_at || now();
|
|
7470
|
+
const lines = [
|
|
7471
|
+
"BEGIN:VCALENDAR",
|
|
7472
|
+
"VERSION:2.0",
|
|
7473
|
+
`PRODID:${options.product_id || "-//hasna//todos local calendar//EN"}`,
|
|
7474
|
+
"CALSCALE:GREGORIAN",
|
|
7475
|
+
`X-WR-CALNAME:${escapeIcs(options.calendar_name || "Hasna Todos")}`
|
|
7476
|
+
];
|
|
7477
|
+
for (const event of events) {
|
|
7478
|
+
const title = options.redact ? `${event.kind} ${event.id.slice(0, 8)}` : event.title;
|
|
7479
|
+
const description = options.redact ? null : event.description;
|
|
7480
|
+
lines.push("BEGIN:VEVENT");
|
|
7481
|
+
lines.push(`UID:${escapeIcs(event.id)}@hasna-todos.local`);
|
|
7482
|
+
lines.push(`DTSTAMP:${icsDate(generatedAt)}`);
|
|
7483
|
+
lines.push(`DTSTART:${icsDate(event.starts_at)}`);
|
|
7484
|
+
if (event.ends_at)
|
|
7485
|
+
lines.push(`DTEND:${icsDate(event.ends_at)}`);
|
|
7486
|
+
else
|
|
7487
|
+
lines.push(`DUE:${icsDate(event.starts_at)}`);
|
|
7488
|
+
lines.push(`SUMMARY:${escapeIcs(title)}`);
|
|
7489
|
+
if (description)
|
|
7490
|
+
lines.push(`DESCRIPTION:${escapeIcs(description)}`);
|
|
7491
|
+
lines.push(`CATEGORIES:${escapeIcs(event.badges.join(","))}`);
|
|
7492
|
+
const rrule = recurrenceToRrule(event.recurrence_rule);
|
|
7493
|
+
if (rrule)
|
|
7494
|
+
lines.push(`RRULE:${rrule}`);
|
|
7495
|
+
lines.push(`X-HASNA-TODOS-KIND:${event.kind}`);
|
|
7496
|
+
if (event.task_id)
|
|
7497
|
+
lines.push(`X-HASNA-TODOS-TASK:${event.task_id}`);
|
|
7498
|
+
if (event.plan_id)
|
|
7499
|
+
lines.push(`X-HASNA-TODOS-PLAN:${event.plan_id}`);
|
|
7500
|
+
if (event.run_id)
|
|
7501
|
+
lines.push(`X-HASNA-TODOS-RUN:${event.run_id}`);
|
|
7502
|
+
lines.push("END:VEVENT");
|
|
7503
|
+
}
|
|
7504
|
+
lines.push("END:VCALENDAR");
|
|
7505
|
+
return {
|
|
7506
|
+
filename: "todos-calendar.ics",
|
|
7507
|
+
content: lines.map(foldLine).join(`\r
|
|
7508
|
+
`) + `\r
|
|
7509
|
+
`,
|
|
7510
|
+
events
|
|
7511
|
+
};
|
|
7512
|
+
}
|
|
7513
|
+
function unescapeIcs(value) {
|
|
7514
|
+
return value.replace(/\\n/g, `
|
|
7515
|
+
`).replace(/\\,/g, ",").replace(/\\;/g, ";").replace(/\\\\/g, "\\");
|
|
7516
|
+
}
|
|
7517
|
+
function parseIcsDate(value) {
|
|
7518
|
+
const match = value.match(/^(\d{4})(\d{2})(\d{2})T?(\d{2})?(\d{2})?(\d{2})?Z?$/);
|
|
7519
|
+
if (!match)
|
|
7520
|
+
return value;
|
|
7521
|
+
return new Date(Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]), Number(match[4] || "0"), Number(match[5] || "0"), Number(match[6] || "0"))).toISOString();
|
|
7522
|
+
}
|
|
7523
|
+
function importCalendarIcs(content, db) {
|
|
7524
|
+
const d = db || getDatabase();
|
|
7525
|
+
const unfolded = content.replace(/\r?\n[ \t]/g, "");
|
|
7526
|
+
const blocks = unfolded.match(/BEGIN:VEVENT[\s\S]*?END:VEVENT/g) || [];
|
|
7527
|
+
const items = [];
|
|
7528
|
+
let skipped = 0;
|
|
7529
|
+
for (const block of blocks) {
|
|
7530
|
+
const fields = new Map;
|
|
7531
|
+
for (const rawLine of block.split(/\r?\n/)) {
|
|
7532
|
+
const index = rawLine.indexOf(":");
|
|
7533
|
+
if (index <= 0)
|
|
7534
|
+
continue;
|
|
7535
|
+
const key = rawLine.slice(0, index).split(";")[0].toUpperCase();
|
|
7536
|
+
fields.set(key, rawLine.slice(index + 1));
|
|
7537
|
+
}
|
|
7538
|
+
const title = unescapeIcs(fields.get("SUMMARY") || "");
|
|
7539
|
+
const startsAt = fields.get("DTSTART") || fields.get("DUE");
|
|
7540
|
+
if (!title || !startsAt) {
|
|
7541
|
+
skipped++;
|
|
7542
|
+
continue;
|
|
7543
|
+
}
|
|
7544
|
+
const item = createCalendarItem({
|
|
7545
|
+
kind: "imported",
|
|
7546
|
+
title,
|
|
7547
|
+
description: fields.has("DESCRIPTION") ? unescapeIcs(fields.get("DESCRIPTION")) : undefined,
|
|
7548
|
+
starts_at: parseIcsDate(startsAt),
|
|
7549
|
+
ends_at: fields.has("DTEND") ? parseIcsDate(fields.get("DTEND")) : undefined,
|
|
7550
|
+
recurrence_rule: fields.get("RRULE"),
|
|
7551
|
+
metadata: { uid: fields.get("UID") || null, imported_from: "ics" }
|
|
7552
|
+
}, d);
|
|
7553
|
+
items.push(item);
|
|
7554
|
+
}
|
|
7555
|
+
return { imported: items.length, skipped, items };
|
|
7556
|
+
}
|
|
5303
7557
|
// src/db/agents.ts
|
|
5304
7558
|
init_database();
|
|
5305
7559
|
|
|
@@ -5895,46 +8149,6 @@ function ensureTaskList(name, slug, projectId, db) {
|
|
|
5895
8149
|
return createTaskList({ name, slug, project_id: projectId }, d);
|
|
5896
8150
|
}
|
|
5897
8151
|
|
|
5898
|
-
// src/db/comments.ts
|
|
5899
|
-
init_types();
|
|
5900
|
-
init_database();
|
|
5901
|
-
function addComment(input, db) {
|
|
5902
|
-
const d = db || getDatabase();
|
|
5903
|
-
if (!getTask(input.task_id, d)) {
|
|
5904
|
-
throw new TaskNotFoundError(input.task_id);
|
|
5905
|
-
}
|
|
5906
|
-
const id = uuid();
|
|
5907
|
-
const timestamp = now();
|
|
5908
|
-
d.run(`INSERT INTO task_comments (id, task_id, agent_id, session_id, content, type, progress_pct, created_at)
|
|
5909
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
5910
|
-
id,
|
|
5911
|
-
input.task_id,
|
|
5912
|
-
input.agent_id || null,
|
|
5913
|
-
input.session_id || null,
|
|
5914
|
-
input.content,
|
|
5915
|
-
input.type || "comment",
|
|
5916
|
-
input.progress_pct ?? null,
|
|
5917
|
-
timestamp
|
|
5918
|
-
]);
|
|
5919
|
-
return getComment(id, d);
|
|
5920
|
-
}
|
|
5921
|
-
function logProgress(taskId, message, pct, agentId, db) {
|
|
5922
|
-
return addComment({ task_id: taskId, content: message, type: "progress", progress_pct: pct, agent_id: agentId }, db);
|
|
5923
|
-
}
|
|
5924
|
-
function getComment(id, db) {
|
|
5925
|
-
const d = db || getDatabase();
|
|
5926
|
-
return d.query("SELECT * FROM task_comments WHERE id = ?").get(id);
|
|
5927
|
-
}
|
|
5928
|
-
function listComments(taskId, db) {
|
|
5929
|
-
const d = db || getDatabase();
|
|
5930
|
-
return d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(taskId);
|
|
5931
|
-
}
|
|
5932
|
-
function deleteComment(id, db) {
|
|
5933
|
-
const d = db || getDatabase();
|
|
5934
|
-
const result = d.run("DELETE FROM task_comments WHERE id = ?", [id]);
|
|
5935
|
-
return result.changes > 0;
|
|
5936
|
-
}
|
|
5937
|
-
|
|
5938
8152
|
// src/storage/local-sqlite.ts
|
|
5939
8153
|
init_database();
|
|
5940
8154
|
function createLocalSqliteTodosStorageAdapter(options = {}) {
|