@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.
Files changed (146) hide show
  1. package/README.md +573 -11
  2. package/dist/cli/commands/agent-reliability-commands.d.ts +3 -0
  3. package/dist/cli/commands/agent-reliability-commands.d.ts.map +1 -0
  4. package/dist/cli/commands/audit-ledger-commands.d.ts +3 -0
  5. package/dist/cli/commands/audit-ledger-commands.d.ts.map +1 -0
  6. package/dist/cli/commands/capacity-commands.d.ts +3 -0
  7. package/dist/cli/commands/capacity-commands.d.ts.map +1 -0
  8. package/dist/cli/commands/config-serve-commands.d.ts.map +1 -1
  9. package/dist/cli/commands/environment-snapshots.d.ts +3 -0
  10. package/dist/cli/commands/environment-snapshots.d.ts.map +1 -0
  11. package/dist/cli/commands/knowledge-commands.d.ts +3 -0
  12. package/dist/cli/commands/knowledge-commands.d.ts.map +1 -0
  13. package/dist/cli/commands/local-snapshot-commands.d.ts +3 -0
  14. package/dist/cli/commands/local-snapshot-commands.d.ts.map +1 -0
  15. package/dist/cli/commands/machines.d.ts.map +1 -1
  16. package/dist/cli/commands/mcp-hooks-commands.d.ts.map +1 -1
  17. package/dist/cli/commands/onboarding-commands.d.ts +3 -0
  18. package/dist/cli/commands/onboarding-commands.d.ts.map +1 -0
  19. package/dist/cli/commands/project-commands.d.ts.map +1 -1
  20. package/dist/cli/commands/query-commands.d.ts.map +1 -1
  21. package/dist/cli/commands/release-compatibility-commands.d.ts +3 -0
  22. package/dist/cli/commands/release-compatibility-commands.d.ts.map +1 -0
  23. package/dist/cli/commands/retrospective-commands.d.ts +3 -0
  24. package/dist/cli/commands/retrospective-commands.d.ts.map +1 -0
  25. package/dist/cli/commands/review-queue-commands.d.ts +3 -0
  26. package/dist/cli/commands/review-queue-commands.d.ts.map +1 -0
  27. package/dist/cli/commands/risk-commands.d.ts +3 -0
  28. package/dist/cli/commands/risk-commands.d.ts.map +1 -0
  29. package/dist/cli/commands/roadmap-commands.d.ts +3 -0
  30. package/dist/cli/commands/roadmap-commands.d.ts.map +1 -0
  31. package/dist/cli/commands/sdk-fixture-commands.d.ts +3 -0
  32. package/dist/cli/commands/sdk-fixture-commands.d.ts.map +1 -0
  33. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  34. package/dist/cli/index.js +26227 -9412
  35. package/dist/cli-mcp-parity.d.ts +1 -1
  36. package/dist/cli-mcp-parity.d.ts.map +1 -1
  37. package/dist/contracts.d.ts +19 -0
  38. package/dist/contracts.d.ts.map +1 -1
  39. package/dist/contracts.js +10174 -679
  40. package/dist/db/agent-metrics.d.ts +101 -0
  41. package/dist/db/agent-metrics.d.ts.map +1 -1
  42. package/dist/db/boards.d.ts +56 -0
  43. package/dist/db/boards.d.ts.map +1 -0
  44. package/dist/db/calendar.d.ts +52 -0
  45. package/dist/db/calendar.d.ts.map +1 -0
  46. package/dist/db/comments.d.ts.map +1 -1
  47. package/dist/db/handoffs.d.ts +25 -0
  48. package/dist/db/handoffs.d.ts.map +1 -1
  49. package/dist/db/machines.d.ts +19 -6
  50. package/dist/db/machines.d.ts.map +1 -1
  51. package/dist/db/migrations.d.ts.map +1 -1
  52. package/dist/db/project-knowledge.d.ts +88 -0
  53. package/dist/db/project-knowledge.d.ts.map +1 -0
  54. package/dist/db/project-risks.d.ts +139 -0
  55. package/dist/db/project-risks.d.ts.map +1 -0
  56. package/dist/db/retrospectives.d.ts +98 -0
  57. package/dist/db/retrospectives.d.ts.map +1 -0
  58. package/dist/db/schema.d.ts.map +1 -1
  59. package/dist/db/task-crud.d.ts.map +1 -1
  60. package/dist/db/task-relations.d.ts +69 -9
  61. package/dist/db/task-relations.d.ts.map +1 -1
  62. package/dist/db/tasks.d.ts +6 -2
  63. package/dist/db/tasks.d.ts.map +1 -1
  64. package/dist/index.d.ts +62 -11
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +19485 -8856
  67. package/dist/json-contracts.d.ts.map +1 -1
  68. package/dist/lib/agent-replay-simulator.d.ts +66 -0
  69. package/dist/lib/agent-replay-simulator.d.ts.map +1 -0
  70. package/dist/lib/audit-ledger.d.ts +59 -0
  71. package/dist/lib/audit-ledger.d.ts.map +1 -0
  72. package/dist/lib/branch-work-plans.d.ts +46 -0
  73. package/dist/lib/branch-work-plans.d.ts.map +1 -0
  74. package/dist/lib/capacity-forecasts.d.ts +70 -0
  75. package/dist/lib/capacity-forecasts.d.ts.map +1 -0
  76. package/dist/lib/config.d.ts +179 -0
  77. package/dist/lib/config.d.ts.map +1 -1
  78. package/dist/lib/context-packs.d.ts +26 -3
  79. package/dist/lib/context-packs.d.ts.map +1 -1
  80. package/dist/lib/environment-snapshots.d.ts +111 -0
  81. package/dist/lib/environment-snapshots.d.ts.map +1 -0
  82. package/dist/lib/event-hooks.d.ts +1 -1
  83. package/dist/lib/event-hooks.d.ts.map +1 -1
  84. package/dist/lib/external-issue-importers.d.ts +60 -0
  85. package/dist/lib/external-issue-importers.d.ts.map +1 -0
  86. package/dist/lib/extract.d.ts +57 -0
  87. package/dist/lib/extract.d.ts.map +1 -1
  88. package/dist/lib/local-bridge.d.ts +3 -1
  89. package/dist/lib/local-bridge.d.ts.map +1 -1
  90. package/dist/lib/local-extensions.d.ts +75 -0
  91. package/dist/lib/local-extensions.d.ts.map +1 -0
  92. package/dist/lib/local-notifications.d.ts +55 -0
  93. package/dist/lib/local-notifications.d.ts.map +1 -0
  94. package/dist/lib/local-snapshots.d.ts +66 -0
  95. package/dist/lib/local-snapshots.d.ts.map +1 -0
  96. package/dist/lib/mention-resolver.d.ts +43 -0
  97. package/dist/lib/mention-resolver.d.ts.map +1 -0
  98. package/dist/lib/natural-language-intake.d.ts +56 -0
  99. package/dist/lib/natural-language-intake.d.ts.map +1 -0
  100. package/dist/lib/onboarding-fixtures.d.ts +31 -0
  101. package/dist/lib/onboarding-fixtures.d.ts.map +1 -0
  102. package/dist/lib/public-release-gate.d.ts +7 -0
  103. package/dist/lib/public-release-gate.d.ts.map +1 -1
  104. package/dist/lib/redaction.d.ts +9 -0
  105. package/dist/lib/redaction.d.ts.map +1 -1
  106. package/dist/lib/release-compatibility.d.ts +59 -0
  107. package/dist/lib/release-compatibility.d.ts.map +1 -0
  108. package/dist/lib/release-notes.d.ts +81 -0
  109. package/dist/lib/release-notes.d.ts.map +1 -0
  110. package/dist/lib/retention-cleanup.d.ts +63 -0
  111. package/dist/lib/retention-cleanup.d.ts.map +1 -0
  112. package/dist/lib/review-queues.d.ts +98 -0
  113. package/dist/lib/review-queues.d.ts.map +1 -0
  114. package/dist/lib/roadmaps.d.ts +133 -0
  115. package/dist/lib/roadmaps.d.ts.map +1 -0
  116. package/dist/lib/sdk-integration-fixtures.d.ts +65 -0
  117. package/dist/lib/sdk-integration-fixtures.d.ts.map +1 -0
  118. package/dist/lib/terminal-notifications.d.ts +53 -0
  119. package/dist/lib/terminal-notifications.d.ts.map +1 -0
  120. package/dist/lib/todos-md.d.ts.map +1 -1
  121. package/dist/lib/workflow-prompts.d.ts +38 -0
  122. package/dist/lib/workflow-prompts.d.ts.map +1 -0
  123. package/dist/mcp/index.d.ts.map +1 -1
  124. package/dist/mcp/index.js +20925 -9542
  125. package/dist/mcp/token-utils.d.ts.map +1 -1
  126. package/dist/mcp/tools/code-tools.d.ts.map +1 -1
  127. package/dist/mcp/tools/environment-snapshots.d.ts +8 -0
  128. package/dist/mcp/tools/environment-snapshots.d.ts.map +1 -0
  129. package/dist/mcp/tools/machines.d.ts.map +1 -1
  130. package/dist/mcp/tools/task-adv-tools.d.ts.map +1 -1
  131. package/dist/mcp/tools/task-auto-tools.d.ts.map +1 -1
  132. package/dist/mcp/tools/task-crud.d.ts.map +1 -1
  133. package/dist/mcp/tools/task-meta-tools.d.ts.map +1 -1
  134. package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
  135. package/dist/mcp/tools/task-rel-tools.d.ts.map +1 -1
  136. package/dist/mcp/tools/task-resources.d.ts.map +1 -1
  137. package/dist/mcp/tools/workflow-prompts.d.ts +3 -0
  138. package/dist/mcp/tools/workflow-prompts.d.ts.map +1 -0
  139. package/dist/mcp.js +97 -2
  140. package/dist/registry.js +14462 -5998
  141. package/dist/release-provenance.json +3 -3
  142. package/dist/server/index.js +493 -123
  143. package/dist/storage.js +2353 -139
  144. package/dist/types/index.d.ts +214 -0
  145. package/dist/types/index.d.ts.map +1 -1
  146. 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 { hostname as osHostname, platform as osPlatform } from "os";
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: row.metadata ? JSON.parse(row.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 = resolve(startDir);
2363
+ let dir = resolve2(startDir);
1861
2364
  while (true) {
1862
2365
  const candidate = join(dir, ".todos", "todos.db");
1863
- if (existsSync(candidate))
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 = resolve(startDir);
2376
+ let dir = resolve2(startDir);
1874
2377
  while (true) {
1875
- if (existsSync(join(dir, ".git")))
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 (!existsSync(newPath) && existsSync(legacyPath)) {
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(resolve(filePath));
1916
- if (!existsSync(dir)) {
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 existsSync3 } from "fs";
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 existsSync2, mkdirSync as mkdirSync2, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
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 (!existsSync2(dir))
2523
+ if (!existsSync3(dir))
2021
2524
  mkdirSync2(dir, { recursive: true });
2022
2525
  }
2023
2526
  function listJsonFiles(dir) {
2024
- if (!existsSync2(dir))
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 (!existsSync2(path))
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 (!existsSync3(newConfig) && existsSync3(legacyConfig))
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 (!existsSync3(getConfigPath())) {
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 resolve4 } from "path";
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
- return value.replace(/\b(AKIA|ASIA)[0-9A-Z]{16}\b/g, "[REDACTED_AWS_KEY]").replace(/-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g, "[REDACTED_PRIVATE_KEY]").replace(/\bsk-[A-Za-z0-9_-]{12,}\b/g, "[REDACTED_TOKEN]").replace(/\b([A-Za-z0-9_]*(?:API_KEY|TOKEN|SECRET|PASSWORD)[A-Za-z0-9_]*)\s*=\s*['"]?[^'"\s]{8,}/gi, "$1=[REDACTED]").replace(/\b(bearer)\s+[A-Za-z0-9._~+/=-]{12,}/gi, "$1 [REDACTED]");
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 (/api[_-]?key|token|secret|password/i.test(key)) {
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 resolve3 } from "path";
3025
+ import { relative as relative2, resolve as resolve4 } from "path";
2459
3026
 
2460
3027
  // src/lib/workspace-trust.ts
2461
- import { relative, resolve as resolve2 } from "path";
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 resolve2(path);
3074
+ return resolve3(path);
2508
3075
  }
2509
- function unique(values) {
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: unique(input.command_allowlist ?? base.command_allowlist ?? PRESET_DEFAULTS[preset].command_allowlist),
2570
- command_denylist: unique(input.command_denylist ?? base.command_denylist ?? PRESET_DEFAULTS[preset].command_denylist),
2571
- tool_permissions: unique(input.tool_permissions ?? base.tool_permissions ?? PRESET_DEFAULTS[preset].tool_permissions),
2572
- write_scopes: unique(input.write_scopes ?? base.write_scopes ?? PRESET_DEFAULTS[preset].write_scopes),
2573
- env_redactions: unique(input.env_redactions ?? base.env_redactions ?? PRESET_DEFAULTS[preset].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 = unique([...DEFAULT_ENV_REDACTIONS, ...profile.env_redactions]).map((item) => item.toUpperCase());
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 resolve3(path);
3211
+ return resolve4(path);
2645
3212
  }
2646
- function unique2(values) {
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 = unique2([...DEFAULT_ENV_REDACTIONS2, ...profile.env_redactions]).map((item) => item.toUpperCase());
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: unique2(input.command_allowlist ?? base.command_allowlist),
2736
- command_denylist: unique2(input.command_denylist ?? base.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: unique2(input.write_scopes ?? base.write_scopes),
2739
- env_allowlist: unique2(input.env_allowlist ?? base.env_allowlist),
2740
- env_redactions: unique2(input.env_redactions ?? base.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 = unique2(reasons);
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 = resolve4(hook.file_path);
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 dueAt = nextOccurrence(completedTask.recurrence_rule, new Date);
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 = new Date().toISOString();
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 logCost(taskId, tokens, usd, db) {
5804
+ function getEscalatedTasks(opts = {}, db, at = new Date) {
5225
5805
  const d = db || getDatabase();
5226
- d.run("UPDATE tasks SET cost_tokens = cost_tokens + ?, cost_usd = cost_usd + ?, updated_at = ? WHERE id = ?", [tokens, usd, now(), taskId]);
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
- // src/db/plans.ts
5229
- init_types();
5230
- init_database();
5231
- function createPlan(input, db) {
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 timestamp = now();
5235
- d.run(`INSERT INTO plans (id, project_id, task_list_id, agent_id, name, description, status, created_at, updated_at)
5236
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
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.project_id || null,
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 getPlan(id, db) {
6914
+ function getTaskVerifications(taskId, db) {
5250
6915
  const d = db || getDatabase();
5251
- const row = d.query("SELECT * FROM plans WHERE id = ?").get(id);
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 listPlans(projectId, db) {
6918
+ function getTaskTraceability(taskId, db) {
5255
6919
  const d = db || getDatabase();
5256
- if (projectId) {
5257
- return d.query("SELECT * FROM plans WHERE project_id = ? ORDER BY created_at DESC").all(projectId);
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 updatePlan(id, input, db) {
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 plan = getPlan(id, d);
5264
- if (!plan)
5265
- throw new PlanNotFoundError(id);
5266
- const sets = ["updated_at = ?"];
5267
- const params = [now()];
5268
- if (input.name !== undefined) {
5269
- sets.push("name = ?");
5270
- params.push(input.name);
5271
- }
5272
- if (input.description !== undefined) {
5273
- sets.push("description = ?");
5274
- params.push(input.description);
5275
- }
5276
- if (input.status !== undefined) {
5277
- sets.push("status = ?");
5278
- params.push(input.status);
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
- if (input.task_list_id !== undefined) {
5281
- sets.push("task_list_id = ?");
5282
- params.push(input.task_list_id);
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
- if (input.agent_id !== undefined) {
5285
- sets.push("agent_id = ?");
5286
- params.push(input.agent_id);
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
- params.push(id);
5289
- d.run(`UPDATE plans SET ${sets.join(", ")} WHERE id = ?`, params);
5290
- const updated = getPlan(id, d);
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: "plan.updated",
5293
- payload: { id, old_status: plan.status, new_status: updated.status, name: updated.name, project_id: updated.project_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 deletePlan(id, db) {
7175
+ function listTaskRuns(taskId, db) {
5298
7176
  const d = db || getDatabase();
5299
- const result = d.run("DELETE FROM plans WHERE id = ?", [id]);
5300
- return result.changes > 0;
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 = {}) {