@hasna/todos 0.11.44 → 0.11.46

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 (167) hide show
  1. package/README.md +695 -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/help-commands.d.ts +3 -0
  10. package/dist/cli/commands/help-commands.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-backup-commands.d.ts +3 -0
  14. package/dist/cli/commands/local-backup-commands.d.ts.map +1 -0
  15. package/dist/cli/commands/local-snapshot-commands.d.ts +3 -0
  16. package/dist/cli/commands/local-snapshot-commands.d.ts.map +1 -0
  17. package/dist/cli/commands/machines.d.ts.map +1 -1
  18. package/dist/cli/commands/mcp-hooks-commands.d.ts.map +1 -1
  19. package/dist/cli/commands/onboarding-commands.d.ts +3 -0
  20. package/dist/cli/commands/onboarding-commands.d.ts.map +1 -0
  21. package/dist/cli/commands/project-commands.d.ts.map +1 -1
  22. package/dist/cli/commands/query-commands.d.ts.map +1 -1
  23. package/dist/cli/commands/release-compatibility-commands.d.ts +3 -0
  24. package/dist/cli/commands/release-compatibility-commands.d.ts.map +1 -0
  25. package/dist/cli/commands/retrospective-commands.d.ts +3 -0
  26. package/dist/cli/commands/retrospective-commands.d.ts.map +1 -0
  27. package/dist/cli/commands/review-queue-commands.d.ts +3 -0
  28. package/dist/cli/commands/review-queue-commands.d.ts.map +1 -0
  29. package/dist/cli/commands/risk-commands.d.ts +3 -0
  30. package/dist/cli/commands/risk-commands.d.ts.map +1 -0
  31. package/dist/cli/commands/roadmap-commands.d.ts +3 -0
  32. package/dist/cli/commands/roadmap-commands.d.ts.map +1 -0
  33. package/dist/cli/commands/scale-hardening-commands.d.ts +3 -0
  34. package/dist/cli/commands/scale-hardening-commands.d.ts.map +1 -0
  35. package/dist/cli/commands/sdk-fixture-commands.d.ts +3 -0
  36. package/dist/cli/commands/sdk-fixture-commands.d.ts.map +1 -0
  37. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  38. package/dist/cli/commands/usage-ledger-commands.d.ts +3 -0
  39. package/dist/cli/commands/usage-ledger-commands.d.ts.map +1 -0
  40. package/dist/cli/components/Dashboard.d.ts.map +1 -1
  41. package/dist/cli/index.js +39446 -19769
  42. package/dist/cli-mcp-parity.d.ts +1 -1
  43. package/dist/cli-mcp-parity.d.ts.map +1 -1
  44. package/dist/contracts.d.ts +25 -0
  45. package/dist/contracts.d.ts.map +1 -1
  46. package/dist/contracts.js +14818 -3885
  47. package/dist/db/agent-metrics.d.ts +101 -0
  48. package/dist/db/agent-metrics.d.ts.map +1 -1
  49. package/dist/db/agent-names.d.ts +2 -1
  50. package/dist/db/agent-names.d.ts.map +1 -1
  51. package/dist/db/boards.d.ts +56 -0
  52. package/dist/db/boards.d.ts.map +1 -0
  53. package/dist/db/calendar.d.ts +52 -0
  54. package/dist/db/calendar.d.ts.map +1 -0
  55. package/dist/db/comments.d.ts.map +1 -1
  56. package/dist/db/handoffs.d.ts +25 -0
  57. package/dist/db/handoffs.d.ts.map +1 -1
  58. package/dist/db/machines.d.ts +19 -6
  59. package/dist/db/machines.d.ts.map +1 -1
  60. package/dist/db/migrations.d.ts.map +1 -1
  61. package/dist/db/project-knowledge.d.ts +88 -0
  62. package/dist/db/project-knowledge.d.ts.map +1 -0
  63. package/dist/db/project-risks.d.ts +139 -0
  64. package/dist/db/project-risks.d.ts.map +1 -0
  65. package/dist/db/retrospectives.d.ts +98 -0
  66. package/dist/db/retrospectives.d.ts.map +1 -0
  67. package/dist/db/schema.d.ts.map +1 -1
  68. package/dist/db/task-crud.d.ts.map +1 -1
  69. package/dist/db/task-relations.d.ts +69 -9
  70. package/dist/db/task-relations.d.ts.map +1 -1
  71. package/dist/db/task-runs.d.ts +3 -0
  72. package/dist/db/task-runs.d.ts.map +1 -1
  73. package/dist/db/tasks.d.ts +6 -2
  74. package/dist/db/tasks.d.ts.map +1 -1
  75. package/dist/index.d.ts +74 -11
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +24662 -11829
  78. package/dist/json-contracts.d.ts.map +1 -1
  79. package/dist/lib/agent-replay-simulator.d.ts +66 -0
  80. package/dist/lib/agent-replay-simulator.d.ts.map +1 -0
  81. package/dist/lib/audit-ledger.d.ts +59 -0
  82. package/dist/lib/audit-ledger.d.ts.map +1 -0
  83. package/dist/lib/branch-work-plans.d.ts +46 -0
  84. package/dist/lib/branch-work-plans.d.ts.map +1 -0
  85. package/dist/lib/capacity-forecasts.d.ts +70 -0
  86. package/dist/lib/capacity-forecasts.d.ts.map +1 -0
  87. package/dist/lib/cli-help.d.ts +38 -0
  88. package/dist/lib/cli-help.d.ts.map +1 -0
  89. package/dist/lib/config.d.ts +217 -0
  90. package/dist/lib/config.d.ts.map +1 -1
  91. package/dist/lib/context-packs.d.ts +26 -3
  92. package/dist/lib/context-packs.d.ts.map +1 -1
  93. package/dist/lib/event-hooks.d.ts +1 -1
  94. package/dist/lib/event-hooks.d.ts.map +1 -1
  95. package/dist/lib/external-issue-importers.d.ts +60 -0
  96. package/dist/lib/external-issue-importers.d.ts.map +1 -0
  97. package/dist/lib/extract.d.ts +57 -0
  98. package/dist/lib/extract.d.ts.map +1 -1
  99. package/dist/lib/local-backups.d.ts +129 -0
  100. package/dist/lib/local-backups.d.ts.map +1 -0
  101. package/dist/lib/local-bridge.d.ts +3 -1
  102. package/dist/lib/local-bridge.d.ts.map +1 -1
  103. package/dist/lib/local-extensions.d.ts +92 -0
  104. package/dist/lib/local-extensions.d.ts.map +1 -0
  105. package/dist/lib/local-notifications.d.ts +55 -0
  106. package/dist/lib/local-notifications.d.ts.map +1 -0
  107. package/dist/lib/local-reports.d.ts +149 -0
  108. package/dist/lib/local-reports.d.ts.map +1 -0
  109. package/dist/lib/local-snapshots.d.ts +66 -0
  110. package/dist/lib/local-snapshots.d.ts.map +1 -0
  111. package/dist/lib/mention-resolver.d.ts +43 -0
  112. package/dist/lib/mention-resolver.d.ts.map +1 -0
  113. package/dist/lib/natural-language-intake.d.ts +56 -0
  114. package/dist/lib/natural-language-intake.d.ts.map +1 -0
  115. package/dist/lib/onboarding-fixtures.d.ts +31 -0
  116. package/dist/lib/onboarding-fixtures.d.ts.map +1 -0
  117. package/dist/lib/public-release-gate.d.ts +7 -0
  118. package/dist/lib/public-release-gate.d.ts.map +1 -1
  119. package/dist/lib/redaction.d.ts +9 -0
  120. package/dist/lib/redaction.d.ts.map +1 -1
  121. package/dist/lib/release-compatibility.d.ts +59 -0
  122. package/dist/lib/release-compatibility.d.ts.map +1 -0
  123. package/dist/lib/release-notes.d.ts +81 -0
  124. package/dist/lib/release-notes.d.ts.map +1 -0
  125. package/dist/lib/retention-cleanup.d.ts +63 -0
  126. package/dist/lib/retention-cleanup.d.ts.map +1 -0
  127. package/dist/lib/review-queues.d.ts +98 -0
  128. package/dist/lib/review-queues.d.ts.map +1 -0
  129. package/dist/lib/roadmaps.d.ts +133 -0
  130. package/dist/lib/roadmaps.d.ts.map +1 -0
  131. package/dist/lib/scale-hardening.d.ts +74 -0
  132. package/dist/lib/scale-hardening.d.ts.map +1 -0
  133. package/dist/lib/sdk-integration-fixtures.d.ts +65 -0
  134. package/dist/lib/sdk-integration-fixtures.d.ts.map +1 -0
  135. package/dist/lib/terminal-notifications.d.ts +53 -0
  136. package/dist/lib/terminal-notifications.d.ts.map +1 -0
  137. package/dist/lib/todos-md.d.ts.map +1 -1
  138. package/dist/lib/tui-dashboard.d.ts +49 -0
  139. package/dist/lib/tui-dashboard.d.ts.map +1 -0
  140. package/dist/lib/usage-ledger.d.ts +82 -0
  141. package/dist/lib/usage-ledger.d.ts.map +1 -0
  142. package/dist/lib/workflow-prompts.d.ts +38 -0
  143. package/dist/lib/workflow-prompts.d.ts.map +1 -0
  144. package/dist/lib/workflow-states.d.ts +70 -0
  145. package/dist/lib/workflow-states.d.ts.map +1 -0
  146. package/dist/mcp/index.d.ts.map +1 -1
  147. package/dist/mcp/index.js +25922 -13067
  148. package/dist/mcp/token-utils.d.ts.map +1 -1
  149. package/dist/mcp/tools/code-tools.d.ts.map +1 -1
  150. package/dist/mcp/tools/machines.d.ts.map +1 -1
  151. package/dist/mcp/tools/task-adv-tools.d.ts.map +1 -1
  152. package/dist/mcp/tools/task-auto-tools.d.ts.map +1 -1
  153. package/dist/mcp/tools/task-crud.d.ts.map +1 -1
  154. package/dist/mcp/tools/task-meta-tools.d.ts.map +1 -1
  155. package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
  156. package/dist/mcp/tools/task-rel-tools.d.ts.map +1 -1
  157. package/dist/mcp/tools/task-resources.d.ts.map +1 -1
  158. package/dist/mcp/tools/workflow-prompts.d.ts +3 -0
  159. package/dist/mcp/tools/workflow-prompts.d.ts.map +1 -0
  160. package/dist/mcp.js +107 -2
  161. package/dist/registry.js +15919 -6050
  162. package/dist/server/index.js +645 -143
  163. package/dist/storage.js +2516 -159
  164. package/dist/types/index.d.ts +214 -0
  165. package/dist/types/index.d.ts.map +1 -1
  166. package/package.json +1 -1
  167. package/dist/release-provenance.json +0 -7
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,
@@ -1610,6 +1839,10 @@ function ensureSchema(db) {
1610
1839
  ensureColumn("dispatches", "machine_id", "TEXT");
1611
1840
  ensureColumn("dispatches", "synced_at", "TEXT");
1612
1841
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_plan ON tasks(plan_id)");
1842
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id)");
1843
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status)");
1844
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_archived_at ON tasks(archived_at)");
1845
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_updated_at ON tasks(updated_at)");
1613
1846
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_task_list ON tasks(task_list_id)");
1614
1847
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_due_at ON tasks(due_at)");
1615
1848
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_short_id ON tasks(short_id) WHERE short_id IS NOT NULL");
@@ -1645,6 +1878,8 @@ function ensureSchema(db) {
1645
1878
  CREATE TABLE task_time_logs (
1646
1879
  id TEXT PRIMARY KEY,
1647
1880
  task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
1881
+ run_id TEXT,
1882
+ focus_session_id TEXT,
1648
1883
  agent_id TEXT,
1649
1884
  started_at TEXT,
1650
1885
  ended_at TEXT,
@@ -1652,9 +1887,77 @@ function ensureSchema(db) {
1652
1887
  notes TEXT,
1653
1888
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
1654
1889
  )`);
1890
+ ensureColumn("task_time_logs", "run_id", "TEXT");
1891
+ ensureColumn("task_time_logs", "focus_session_id", "TEXT");
1655
1892
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_time_logs_task ON task_time_logs(task_id)");
1656
1893
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_time_logs_agent ON task_time_logs(agent_id)");
1894
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_time_logs_run ON task_time_logs(run_id)");
1895
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_time_logs_focus_session ON task_time_logs(focus_session_id)");
1657
1896
  ensureColumn("tasks", "actual_minutes", "INTEGER");
1897
+ ensureTable("focus_sessions", `
1898
+ CREATE TABLE focus_sessions (
1899
+ id TEXT PRIMARY KEY,
1900
+ task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
1901
+ plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
1902
+ run_id TEXT,
1903
+ agent_id TEXT,
1904
+ title TEXT,
1905
+ status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'paused', 'completed', 'cancelled')),
1906
+ started_at TEXT NOT NULL,
1907
+ last_resumed_at TEXT,
1908
+ paused_at TEXT,
1909
+ ended_at TEXT,
1910
+ actual_minutes INTEGER NOT NULL DEFAULT 0,
1911
+ idle_after_minutes INTEGER,
1912
+ notes TEXT,
1913
+ metadata TEXT DEFAULT '{}',
1914
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1915
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1916
+ )`);
1917
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_focus_sessions_task ON focus_sessions(task_id)");
1918
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_focus_sessions_plan ON focus_sessions(plan_id)");
1919
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_focus_sessions_run ON focus_sessions(run_id)");
1920
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_focus_sessions_agent ON focus_sessions(agent_id)");
1921
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_focus_sessions_status ON focus_sessions(status)");
1922
+ ensureTable("task_boards", `
1923
+ CREATE TABLE task_boards (
1924
+ id TEXT PRIMARY KEY,
1925
+ name TEXT NOT NULL UNIQUE,
1926
+ scope TEXT NOT NULL DEFAULT 'tasks' CHECK(scope IN ('tasks', 'plans')),
1927
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
1928
+ task_list_id TEXT REFERENCES task_lists(id) ON DELETE SET NULL,
1929
+ plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
1930
+ agent_id TEXT,
1931
+ lanes TEXT NOT NULL DEFAULT '[]',
1932
+ filters TEXT NOT NULL DEFAULT '{}',
1933
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1934
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1935
+ )`);
1936
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_boards_scope ON task_boards(scope)");
1937
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_boards_project ON task_boards(project_id)");
1938
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_boards_plan ON task_boards(plan_id)");
1939
+ ensureTable("local_calendar_items", `
1940
+ CREATE TABLE local_calendar_items (
1941
+ id TEXT PRIMARY KEY,
1942
+ kind TEXT NOT NULL CHECK(kind IN ('task_due', 'task_sla', 'task_reminder', 'milestone', 'work_block', 'run', 'imported')),
1943
+ title TEXT NOT NULL,
1944
+ description TEXT,
1945
+ starts_at TEXT NOT NULL,
1946
+ ends_at TEXT,
1947
+ timezone TEXT,
1948
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
1949
+ task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
1950
+ plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
1951
+ run_id TEXT,
1952
+ recurrence_rule TEXT,
1953
+ metadata TEXT DEFAULT '{}',
1954
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1955
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1956
+ )`);
1957
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_local_calendar_items_time ON local_calendar_items(starts_at, ends_at)");
1958
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_local_calendar_items_task ON local_calendar_items(task_id)");
1959
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_local_calendar_items_project ON local_calendar_items(project_id)");
1960
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_local_calendar_items_kind ON local_calendar_items(kind)");
1658
1961
  ensureTable("task_watchers", `
1659
1962
  CREATE TABLE task_watchers (
1660
1963
  id TEXT PRIMARY KEY,
@@ -1734,12 +2037,63 @@ var init_schema = __esm(() => {
1734
2037
  });
1735
2038
 
1736
2039
  // src/db/machines.ts
1737
- import { hostname as osHostname, platform as osPlatform } from "os";
2040
+ import { existsSync } from "fs";
2041
+ import { hostname as osHostname, platform as osPlatform, arch as osArch } from "os";
2042
+ import { resolve } from "path";
2043
+ import { spawnSync } from "child_process";
2044
+ function parseMetadata(value) {
2045
+ if (!value)
2046
+ return {};
2047
+ try {
2048
+ const parsed = JSON.parse(value);
2049
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
2050
+ } catch {
2051
+ return {};
2052
+ }
2053
+ }
1738
2054
  function rowToMachine(row) {
1739
2055
  return {
1740
2056
  ...row,
1741
2057
  is_primary: !!row.is_primary,
1742
- metadata: row.metadata ? JSON.parse(row.metadata) : {}
2058
+ metadata: parseMetadata(row.metadata)
2059
+ };
2060
+ }
2061
+ function discoverGitRoot(workspacePath) {
2062
+ const result = spawnSync("git", ["-C", workspacePath, "rev-parse", "--show-toplevel"], {
2063
+ encoding: "utf-8",
2064
+ timeout: 2000
2065
+ });
2066
+ if (result.status !== 0)
2067
+ return;
2068
+ const root = result.stdout.trim();
2069
+ return root || undefined;
2070
+ }
2071
+ function topologyMetadata(input, existing = {}) {
2072
+ const next = { ...existing };
2073
+ const workspacePath = input.workspace_path ? resolve(input.workspace_path) : undefined;
2074
+ const entries = {
2075
+ tailscale_name: input.tailscale_name,
2076
+ tailscale_ip: input.tailscale_ip,
2077
+ lan_address: input.lan_address,
2078
+ workspace_path: workspacePath,
2079
+ git_root: input.git_root ?? (workspacePath ? discoverGitRoot(workspacePath) : undefined),
2080
+ arch: input.arch
2081
+ };
2082
+ for (const [key, value] of Object.entries(entries)) {
2083
+ if (value !== undefined && value !== "")
2084
+ next[key] = value;
2085
+ }
2086
+ return next;
2087
+ }
2088
+ function extractTopology(machine) {
2089
+ const metadata = machine.metadata;
2090
+ return {
2091
+ tailscale_name: typeof metadata["tailscale_name"] === "string" ? metadata["tailscale_name"] : undefined,
2092
+ tailscale_ip: typeof metadata["tailscale_ip"] === "string" ? metadata["tailscale_ip"] : undefined,
2093
+ lan_address: typeof metadata["lan_address"] === "string" ? metadata["lan_address"] : undefined,
2094
+ workspace_path: typeof metadata["workspace_path"] === "string" ? metadata["workspace_path"] : undefined,
2095
+ git_root: typeof metadata["git_root"] === "string" ? metadata["git_root"] : undefined,
2096
+ arch: typeof metadata["arch"] === "string" ? metadata["arch"] : undefined
1743
2097
  };
1744
2098
  }
1745
2099
  function getOrCreateLocalMachine(db) {
@@ -1783,6 +2137,159 @@ function listMachines(db, includeArchived = false) {
1783
2137
  const rows = d.query(query).all();
1784
2138
  return rows.map(rowToMachine);
1785
2139
  }
2140
+ function registerMachine(name, opts, db) {
2141
+ const d = db || getDatabase();
2142
+ const existing = d.query("SELECT * FROM machines WHERE name = ?").get(name);
2143
+ const metadata = topologyMetadata(opts, parseMetadata(existing?.metadata ?? null));
2144
+ if (existing) {
2145
+ d.run("UPDATE machines SET hostname = ?, platform = ?, ssh_address = ?, last_seen_at = ?, metadata = ? WHERE id = ?", [
2146
+ opts.hostname ?? existing.hostname,
2147
+ opts.platform ?? existing.platform,
2148
+ opts.ssh_address ?? existing.ssh_address,
2149
+ now(),
2150
+ JSON.stringify(metadata),
2151
+ existing.id
2152
+ ]);
2153
+ if (opts.primary) {
2154
+ setPrimaryMachine(name, d);
2155
+ }
2156
+ return getMachine(existing.id, d);
2157
+ }
2158
+ const id = uuid();
2159
+ const ts = now();
2160
+ const host = opts.hostname || osHostname();
2161
+ const plat = opts.platform || osPlatform();
2162
+ 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]);
2163
+ if (opts.primary) {
2164
+ setPrimaryMachine(name, d);
2165
+ }
2166
+ return getMachine(id, d);
2167
+ }
2168
+ function updateMachineHeartbeat(idOrName, opts = {}, db) {
2169
+ const d = db || getDatabase();
2170
+ const key = idOrName || process.env["TODOS_MACHINE_NAME"] || osHostname();
2171
+ const row = d.query("SELECT * FROM machines WHERE id = ? OR name = ?").get(key, key);
2172
+ if (!row) {
2173
+ return registerMachine(key, {
2174
+ hostname: opts.hostname ?? osHostname(),
2175
+ platform: opts.platform ?? osPlatform(),
2176
+ arch: opts.arch ?? osArch(),
2177
+ ...opts
2178
+ }, d);
2179
+ }
2180
+ const metadata = topologyMetadata({ arch: osArch(), ...opts }, parseMetadata(row.metadata));
2181
+ const ts = now();
2182
+ d.run("UPDATE machines SET hostname = ?, platform = ?, ssh_address = ?, last_seen_at = ?, metadata = ? WHERE id = ?", [
2183
+ opts.hostname ?? row.hostname ?? osHostname(),
2184
+ opts.platform ?? row.platform ?? osPlatform(),
2185
+ opts.ssh_address ?? row.ssh_address,
2186
+ ts,
2187
+ JSON.stringify(metadata),
2188
+ row.id
2189
+ ]);
2190
+ return getMachine(row.id, d);
2191
+ }
2192
+ function getMachineTopologyDiagnostics(opts = {}, db, at = new Date) {
2193
+ const d = db || getDatabase();
2194
+ const staleAfter = opts.stale_minutes ?? 30;
2195
+ const generatedAt = at.toISOString();
2196
+ const localMachine = getOrCreateLocalMachine(d);
2197
+ const machines = listMachines(d, opts.include_archived ?? true);
2198
+ const machineById = new Map(machines.map((machine) => [machine.id, machine]));
2199
+ const summaries = machines.map((machine) => {
2200
+ const lastSeenMs = Date.parse(machine.last_seen_at);
2201
+ const staleMinutes = Number.isFinite(lastSeenMs) ? Math.max(0, Math.floor((at.getTime() - lastSeenMs) / 60000)) : Number.POSITIVE_INFINITY;
2202
+ return {
2203
+ id: machine.id,
2204
+ name: machine.name,
2205
+ hostname: machine.hostname,
2206
+ platform: machine.platform,
2207
+ ssh_address: machine.ssh_address,
2208
+ is_primary: machine.is_primary,
2209
+ archived_at: machine.archived_at,
2210
+ last_seen_at: machine.last_seen_at,
2211
+ stale: !machine.archived_at && staleMinutes > staleAfter,
2212
+ stale_minutes: Number.isFinite(staleMinutes) ? staleMinutes : staleAfter + 1,
2213
+ topology: extractTopology(machine)
2214
+ };
2215
+ });
2216
+ 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
2217
+ FROM project_machine_paths pmp
2218
+ JOIN projects p ON p.id = pmp.project_id
2219
+ LEFT JOIN machines m ON m.id = pmp.machine_id
2220
+ ORDER BY p.name, pmp.machine_id`).all();
2221
+ const projectRows = d.query("SELECT id, name, path FROM projects ORDER BY name").all();
2222
+ const rowsByProject = new Map;
2223
+ for (const row of pathRows) {
2224
+ const rows = rowsByProject.get(row.project_id) ?? [];
2225
+ rows.push({ ...row, machine_name: row.machine_name ?? row.machine_id });
2226
+ rowsByProject.set(row.project_id, rows);
2227
+ }
2228
+ const pathIssues = [];
2229
+ for (const project of projectRows) {
2230
+ const rows = rowsByProject.get(project.id) ?? [];
2231
+ const localRow = rows.find((row) => row.machine_id === localMachine.id);
2232
+ if (!localRow) {
2233
+ pathIssues.push({
2234
+ type: "missing_local_path",
2235
+ project_id: project.id,
2236
+ project_name: project.name,
2237
+ message: `No machine-local path override is registered for ${project.name} on ${localMachine.name}`
2238
+ });
2239
+ }
2240
+ const distinctPaths = [...new Set(rows.map((row) => row.path))];
2241
+ if (distinctPaths.length > 1) {
2242
+ pathIssues.push({
2243
+ type: "path_mismatch",
2244
+ project_id: project.id,
2245
+ project_name: project.name,
2246
+ paths: rows.map((row) => ({ machine_id: row.machine_id, machine_name: row.machine_name, path: row.path })),
2247
+ message: `${project.name} has ${distinctPaths.length} different machine-local paths`
2248
+ });
2249
+ }
2250
+ if (localRow && !existsSync(localRow.path)) {
2251
+ pathIssues.push({
2252
+ type: "path_missing",
2253
+ project_id: project.id,
2254
+ project_name: project.name,
2255
+ machine_id: localMachine.id,
2256
+ machine_name: localMachine.name,
2257
+ path: localRow.path,
2258
+ message: `Local path does not exist on this machine: ${localRow.path}`
2259
+ });
2260
+ }
2261
+ if (!localRow && project.path && machineById.has(localMachine.id) && !existsSync(project.path)) {
2262
+ pathIssues.push({
2263
+ type: "path_missing",
2264
+ project_id: project.id,
2265
+ project_name: project.name,
2266
+ machine_id: localMachine.id,
2267
+ machine_name: localMachine.name,
2268
+ path: project.path,
2269
+ message: `Project path does not exist on this machine: ${project.path}`
2270
+ });
2271
+ }
2272
+ }
2273
+ return {
2274
+ generated_at: generatedAt,
2275
+ stale_after_minutes: staleAfter,
2276
+ local_machine: localMachine,
2277
+ machines: summaries,
2278
+ stale_machines: summaries.filter((summary) => summary.stale),
2279
+ path_issues: pathIssues
2280
+ };
2281
+ }
2282
+ function setPrimaryMachine(name, db) {
2283
+ const d = db || getDatabase();
2284
+ const row = d.query("SELECT * FROM machines WHERE name = ?").get(name);
2285
+ if (!row)
2286
+ throw new Error(`Machine '${name}' not found`);
2287
+ if (row.archived_at)
2288
+ throw new Error(`Cannot set archived machine '${name}' as primary`);
2289
+ d.run("UPDATE machines SET is_primary = 0 WHERE archived_at IS NULL");
2290
+ d.run("UPDATE machines SET is_primary = 1 WHERE id = ?", [row.id]);
2291
+ return rowToMachine({ ...row, is_primary: 1 });
2292
+ }
1786
2293
  function deleteMachine(id, db) {
1787
2294
  const d = db || getDatabase();
1788
2295
  const row = d.query("SELECT * FROM machines WHERE id = ?").get(id);
@@ -1851,16 +2358,16 @@ __export(exports_database, {
1851
2358
  LOCK_EXPIRY_MINUTES: () => LOCK_EXPIRY_MINUTES
1852
2359
  });
1853
2360
  import { Database } from "bun:sqlite";
1854
- import { existsSync, mkdirSync } from "fs";
1855
- import { dirname, join, resolve } from "path";
2361
+ import { existsSync as existsSync2, mkdirSync } from "fs";
2362
+ import { dirname, join, resolve as resolve2 } from "path";
1856
2363
  function isInMemoryDb(path) {
1857
2364
  return path === ":memory:" || path.startsWith("file::memory:");
1858
2365
  }
1859
2366
  function findNearestTodosDb(startDir) {
1860
- let dir = resolve(startDir);
2367
+ let dir = resolve2(startDir);
1861
2368
  while (true) {
1862
2369
  const candidate = join(dir, ".todos", "todos.db");
1863
- if (existsSync(candidate))
2370
+ if (existsSync2(candidate))
1864
2371
  return candidate;
1865
2372
  const parent = dirname(dir);
1866
2373
  if (parent === dir)
@@ -1870,9 +2377,9 @@ function findNearestTodosDb(startDir) {
1870
2377
  return null;
1871
2378
  }
1872
2379
  function findGitRoot(startDir) {
1873
- let dir = resolve(startDir);
2380
+ let dir = resolve2(startDir);
1874
2381
  while (true) {
1875
- if (existsSync(join(dir, ".git")))
2382
+ if (existsSync2(join(dir, ".git")))
1876
2383
  return dir;
1877
2384
  const parent = dirname(dir);
1878
2385
  if (parent === dir)
@@ -1901,7 +2408,7 @@ function getDbPath() {
1901
2408
  const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
1902
2409
  const newPath = join(home, ".hasna", "todos", "todos.db");
1903
2410
  const legacyPath = join(home, ".todos", "todos.db");
1904
- if (!existsSync(newPath) && existsSync(legacyPath)) {
2411
+ if (!existsSync2(newPath) && existsSync2(legacyPath)) {
1905
2412
  return legacyPath;
1906
2413
  }
1907
2414
  return newPath;
@@ -1912,8 +2419,8 @@ function getDatabasePath() {
1912
2419
  function ensureDir(filePath) {
1913
2420
  if (isInMemoryDb(filePath))
1914
2421
  return;
1915
- const dir = dirname(resolve(filePath));
1916
- if (!existsSync(dir)) {
2422
+ const dir = dirname(resolve2(filePath));
2423
+ if (!existsSync2(dir)) {
1917
2424
  mkdirSync(dir, { recursive: true });
1918
2425
  }
1919
2426
  }
@@ -1998,7 +2505,7 @@ var LOCK_EXPIRY_MINUTES = 30, _db = null, ALLOWED_TABLES;
1998
2505
  var init_database = __esm(() => {
1999
2506
  init_schema();
2000
2507
  init_machines();
2001
- ALLOWED_TABLES = new Set(["tasks", "projects", "agents", "plans", "task_lists", "task_templates"]);
2508
+ ALLOWED_TABLES = new Set(["tasks", "projects", "agents", "plans", "task_lists", "task_templates", "project_knowledge_records", "project_risks", "local_retrospectives"]);
2002
2509
  });
2003
2510
 
2004
2511
  // src/db/task-crud.ts
@@ -2009,19 +2516,19 @@ init_database();
2009
2516
  init_types();
2010
2517
 
2011
2518
  // src/lib/config.ts
2012
- import { existsSync as existsSync3 } from "fs";
2519
+ import { existsSync as existsSync4 } from "fs";
2013
2520
  import { dirname as dirname2, join as join3 } from "path";
2014
2521
 
2015
2522
  // src/lib/sync-utils.ts
2016
- import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
2523
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
2017
2524
  import { join as join2 } from "path";
2018
2525
  var HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
2019
2526
  function ensureDir2(dir) {
2020
- if (!existsSync2(dir))
2527
+ if (!existsSync3(dir))
2021
2528
  mkdirSync2(dir, { recursive: true });
2022
2529
  }
2023
2530
  function listJsonFiles(dir) {
2024
- if (!existsSync2(dir))
2531
+ if (!existsSync3(dir))
2025
2532
  return [];
2026
2533
  return readdirSync(dir).filter((f) => f.endsWith(".json"));
2027
2534
  }
@@ -2038,7 +2545,7 @@ function writeJsonFile(path, data) {
2038
2545
  }
2039
2546
  function readHighWaterMark(dir) {
2040
2547
  const path = join2(dir, ".highwatermark");
2041
- if (!existsSync2(path))
2548
+ if (!existsSync3(path))
2042
2549
  return 1;
2043
2550
  const val = parseInt(readFileSync(path, "utf-8").trim(), 10);
2044
2551
  return isNaN(val) ? 1 : val;
@@ -2072,7 +2579,7 @@ function getTodosGlobalDir() {
2072
2579
  const legacyDir = join3(home, ".todos");
2073
2580
  const newConfig = join3(newDir, "config.json");
2074
2581
  const legacyConfig = join3(legacyDir, "config.json");
2075
- if (!existsSync3(newConfig) && existsSync3(legacyConfig))
2582
+ if (!existsSync4(newConfig) && existsSync4(legacyConfig))
2076
2583
  return legacyDir;
2077
2584
  return newDir;
2078
2585
  }
@@ -2086,7 +2593,7 @@ function normalizeAgent(agent) {
2086
2593
  function loadConfig() {
2087
2594
  if (cached)
2088
2595
  return cached;
2089
- if (!existsSync3(getConfigPath())) {
2596
+ if (!existsSync4(getConfigPath())) {
2090
2597
  cached = {};
2091
2598
  return cached;
2092
2599
  }
@@ -2428,12 +2935,60 @@ function checkCompletionGuard(task, agentId, db, configOverride) {
2428
2935
  // src/lib/event-hooks.ts
2429
2936
  import { createHash, randomUUID } from "crypto";
2430
2937
  import { appendFileSync, mkdirSync as mkdirSync3 } from "fs";
2431
- import { dirname as dirname3, resolve as resolve4 } from "path";
2938
+ import { dirname as dirname3, resolve as resolve5 } from "path";
2432
2939
  import { createConnection } from "net";
2433
2940
 
2434
2941
  // src/lib/redaction.ts
2942
+ var DEFAULT_SECRET_PATTERNS = [
2943
+ { name: "aws-access-key", regex: /\b(AKIA|ASIA)[0-9A-Z]{16}\b/g, replacement: "[REDACTED_AWS_KEY]" },
2944
+ { name: "private-key", regex: /-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g, replacement: "[REDACTED_PRIVATE_KEY]" },
2945
+ { name: "openai-token", regex: /\bsk-[A-Za-z0-9_-]{12,}\b/g, replacement: "[REDACTED_TOKEN]" },
2946
+ { 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]" },
2947
+ { name: "bearer-token", regex: /\b(bearer)\s+[A-Za-z0-9._~+/=-]{12,}/gi, replacement: "$1 [REDACTED]" }
2948
+ ];
2949
+ var DEFAULT_SECRET_KEY_PATTERN = /api[_-]?key|token|secret|password/i;
2950
+ var NON_SECRET_USAGE_KEYS = new Set([
2951
+ "tokens",
2952
+ "total_tokens",
2953
+ "token_count",
2954
+ "input_tokens",
2955
+ "output_tokens",
2956
+ "prompt_tokens",
2957
+ "completion_tokens"
2958
+ ]);
2959
+ function unique(values) {
2960
+ return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
2961
+ }
2962
+ function cloneRegex(regex) {
2963
+ return new RegExp(regex.source, regex.flags.includes("g") ? regex.flags : `${regex.flags}g`);
2964
+ }
2965
+ function customPatterns() {
2966
+ return unique(loadConfig().secret_safety?.redaction_patterns).flatMap((pattern) => {
2967
+ try {
2968
+ return [{ name: `custom:${pattern}`, regex: new RegExp(pattern, "g") }];
2969
+ } catch {
2970
+ return [];
2971
+ }
2972
+ });
2973
+ }
2974
+ function secretPatterns() {
2975
+ return [...customPatterns(), ...DEFAULT_SECRET_PATTERNS];
2976
+ }
2977
+ function isSecretKey(key) {
2978
+ if (NON_SECRET_USAGE_KEYS.has(key.toLowerCase()))
2979
+ return false;
2980
+ if (DEFAULT_SECRET_KEY_PATTERN.test(key))
2981
+ return true;
2982
+ return unique(loadConfig().secret_safety?.redaction_keys).some((pattern) => key.toLowerCase().includes(pattern.toLowerCase()));
2983
+ }
2435
2984
  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]");
2985
+ let redacted = value;
2986
+ for (const pattern of secretPatterns()) {
2987
+ const regex = cloneRegex(pattern.regex);
2988
+ const replacement = pattern.replacement ?? "[REDACTED]";
2989
+ redacted = typeof replacement === "string" ? redacted.replace(regex, replacement) : redacted.replace(regex, replacement);
2990
+ }
2991
+ return redacted;
2437
2992
  }
2438
2993
  function redactValue(value) {
2439
2994
  if (typeof value === "string")
@@ -2443,7 +2998,7 @@ function redactValue(value) {
2443
2998
  if (value && typeof value === "object") {
2444
2999
  const redacted = {};
2445
3000
  for (const [key, child] of Object.entries(value)) {
2446
- if (/api[_-]?key|token|secret|password/i.test(key)) {
3001
+ if (isSecretKey(key)) {
2447
3002
  redacted[key] = "[REDACTED]";
2448
3003
  } else {
2449
3004
  redacted[key] = redactValue(child);
@@ -2453,12 +3008,39 @@ function redactValue(value) {
2453
3008
  }
2454
3009
  return value;
2455
3010
  }
3011
+ function listSecretFindings(value) {
3012
+ const findings = [];
3013
+ for (const pattern of secretPatterns()) {
3014
+ const matches = value.match(cloneRegex(pattern.regex));
3015
+ if (matches?.length)
3016
+ findings.push({ pattern: pattern.name, count: matches.length });
3017
+ }
3018
+ return findings;
3019
+ }
3020
+ function hasSecretFindings(value) {
3021
+ return listSecretFindings(value).length > 0;
3022
+ }
3023
+ function getSecretSafetyConfig() {
3024
+ return {
3025
+ redaction_patterns: unique(loadConfig().secret_safety?.redaction_patterns),
3026
+ redaction_keys: unique(loadConfig().secret_safety?.redaction_keys)
3027
+ };
3028
+ }
3029
+ function upsertSecretSafetyConfig(input) {
3030
+ const config = loadConfig();
3031
+ const next = {
3032
+ redaction_patterns: unique([...config.secret_safety?.redaction_patterns || [], ...input.redaction_patterns || []]),
3033
+ redaction_keys: unique([...config.secret_safety?.redaction_keys || [], ...input.redaction_keys || []])
3034
+ };
3035
+ saveConfig({ ...config, secret_safety: next });
3036
+ return next;
3037
+ }
2456
3038
 
2457
3039
  // src/lib/runner-sandbox.ts
2458
- import { relative as relative2, resolve as resolve3 } from "path";
3040
+ import { relative as relative2, resolve as resolve4 } from "path";
2459
3041
 
2460
3042
  // src/lib/workspace-trust.ts
2461
- import { relative, resolve as resolve2 } from "path";
3043
+ import { relative, resolve as resolve3 } from "path";
2462
3044
  var DEFAULT_DENYLIST = ["rm -rf", "mkfs", "dd if=", "curl | sh", "wget | sh"];
2463
3045
  var DEFAULT_ENV_REDACTIONS = ["API_KEY", "TOKEN", "SECRET", "PASSWORD", "AUTH"];
2464
3046
  var PRESET_DEFAULTS = {
@@ -2504,9 +3086,9 @@ var PRESET_DEFAULTS = {
2504
3086
  }
2505
3087
  };
2506
3088
  function normalizePath(path) {
2507
- return resolve2(path);
3089
+ return resolve3(path);
2508
3090
  }
2509
- function unique(values) {
3091
+ function unique2(values) {
2510
3092
  return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
2511
3093
  }
2512
3094
  function defaultProfile(root, preset) {
@@ -2566,11 +3148,11 @@ function upsertWorkspaceTrustProfile(input) {
2566
3148
  root,
2567
3149
  preset,
2568
3150
  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),
3151
+ command_allowlist: unique2(input.command_allowlist ?? base.command_allowlist ?? PRESET_DEFAULTS[preset].command_allowlist),
3152
+ command_denylist: unique2(input.command_denylist ?? base.command_denylist ?? PRESET_DEFAULTS[preset].command_denylist),
3153
+ tool_permissions: unique2(input.tool_permissions ?? base.tool_permissions ?? PRESET_DEFAULTS[preset].tool_permissions),
3154
+ write_scopes: unique2(input.write_scopes ?? base.write_scopes ?? PRESET_DEFAULTS[preset].write_scopes),
3155
+ env_redactions: unique2(input.env_redactions ?? base.env_redactions ?? PRESET_DEFAULTS[preset].env_redactions),
2574
3156
  require_prompt_for_unsafe: input.require_prompt_for_unsafe ?? base.require_prompt_for_unsafe ?? PRESET_DEFAULTS[preset].require_prompt_for_unsafe,
2575
3157
  created_at: existing?.created_at || timestamp,
2576
3158
  updated_at: timestamp
@@ -2604,7 +3186,7 @@ function writeAllowed(profile, root, writePath) {
2604
3186
  function redactedEnvKeys(profile, env) {
2605
3187
  if (!env)
2606
3188
  return [];
2607
- const patterns = unique([...DEFAULT_ENV_REDACTIONS, ...profile.env_redactions]).map((item) => item.toUpperCase());
3189
+ const patterns = unique2([...DEFAULT_ENV_REDACTIONS, ...profile.env_redactions]).map((item) => item.toUpperCase());
2608
3190
  return Object.keys(env).filter((key) => patterns.some((pattern) => key.toUpperCase().includes(pattern)));
2609
3191
  }
2610
3192
  function checkWorkspacePermission(input = {}) {
@@ -2641,9 +3223,9 @@ function checkWorkspacePermission(input = {}) {
2641
3223
  var DEFAULT_COMMAND_DENYLIST = ["rm -rf", "mkfs", "dd if=", "curl | sh", "wget | sh"];
2642
3224
  var DEFAULT_ENV_REDACTIONS2 = ["API_KEY", "TOKEN", "SECRET", "PASSWORD", "AUTH"];
2643
3225
  function normalizePath2(path) {
2644
- return resolve3(path);
3226
+ return resolve4(path);
2645
3227
  }
2646
- function unique2(values) {
3228
+ function unique3(values) {
2647
3229
  return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
2648
3230
  }
2649
3231
  function configuredProfiles2(config = loadConfig()) {
@@ -2699,7 +3281,7 @@ function profileByName(name, path) {
2699
3281
  function redactedEnvKeys2(profile, env) {
2700
3282
  if (!env)
2701
3283
  return [];
2702
- const patterns = unique2([...DEFAULT_ENV_REDACTIONS2, ...profile.env_redactions]).map((item) => item.toUpperCase());
3284
+ const patterns = unique3([...DEFAULT_ENV_REDACTIONS2, ...profile.env_redactions]).map((item) => item.toUpperCase());
2703
3285
  return Object.keys(env).filter((key) => patterns.some((pattern) => key.toUpperCase().includes(pattern)));
2704
3286
  }
2705
3287
  function omittedEnvKeys(profile, env) {
@@ -2732,12 +3314,12 @@ function upsertRunnerSandboxProfile(input) {
2732
3314
  ...base,
2733
3315
  name: input.name,
2734
3316
  root,
2735
- command_allowlist: unique2(input.command_allowlist ?? base.command_allowlist),
2736
- command_denylist: unique2(input.command_denylist ?? base.command_denylist),
3317
+ command_allowlist: unique3(input.command_allowlist ?? base.command_allowlist),
3318
+ command_denylist: unique3(input.command_denylist ?? base.command_denylist),
2737
3319
  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),
3320
+ write_scopes: unique3(input.write_scopes ?? base.write_scopes),
3321
+ env_allowlist: unique3(input.env_allowlist ?? base.env_allowlist),
3322
+ env_redactions: unique3(input.env_redactions ?? base.env_redactions),
2741
3323
  network_policy: input.network_policy || base.network_policy,
2742
3324
  require_approval: input.require_approval ?? base.require_approval,
2743
3325
  audit_evidence: input.audit_evidence ?? base.audit_evidence,
@@ -2797,7 +3379,7 @@ function checkRunnerSandbox(input = {}) {
2797
3379
  const redacted = redactedEnvKeys2(profile, input.env);
2798
3380
  const omitted = omittedEnvKeys(profile, input.env);
2799
3381
  const effective = Object.keys(input.env || {}).filter((key) => !omitted.includes(key));
2800
- const uniqueReasons = unique2(reasons);
3382
+ const uniqueReasons = unique3(reasons);
2801
3383
  const allowed = uniqueReasons.length === 0;
2802
3384
  return {
2803
3385
  allowed,
@@ -2830,9 +3412,14 @@ var LOCAL_EVENT_TYPES = [
2830
3412
  "task.blocked",
2831
3413
  "task.started",
2832
3414
  "task.completed",
3415
+ "task.due",
3416
+ "task.due_soon",
2833
3417
  "task.failed",
3418
+ "task.sla_breached",
3419
+ "task.stale",
2834
3420
  "task.unblocked",
2835
3421
  "task.status_changed",
3422
+ "calendar.reminder",
2836
3423
  "plan.updated",
2837
3424
  "run.started",
2838
3425
  "run.completed",
@@ -2979,7 +3566,7 @@ async function deliverHook(hook, envelope) {
2979
3566
  if (hook.target === "stdout") {
2980
3567
  output = line.trim();
2981
3568
  } else if (hook.target === "file") {
2982
- const filePath = resolve4(hook.file_path);
3569
+ const filePath = resolve5(hook.file_path);
2983
3570
  mkdirSync3(dirname3(filePath), { recursive: true });
2984
3571
  appendFileSync(filePath, line);
2985
3572
  } else if (hook.target === "socket") {
@@ -3414,8 +4001,8 @@ function createTask(input, db) {
3414
4001
  let id = uuid();
3415
4002
  for (let attempt = 0;attempt < 3; attempt++) {
3416
4003
  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, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
4004
+ 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)
4005
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
3419
4006
  id,
3420
4007
  null,
3421
4008
  input.project_id || null,
@@ -3437,6 +4024,7 @@ function createTask(input, db) {
3437
4024
  timestamp,
3438
4025
  input.due_at || null,
3439
4026
  input.estimated_minutes || null,
4027
+ input.sla_minutes ?? null,
3440
4028
  input.confidence ?? null,
3441
4029
  input.retry_count ?? 0,
3442
4030
  input.max_retries ?? 3,
@@ -3744,6 +4332,10 @@ function updateTask(id, input, db) {
3744
4332
  sets.push("estimated_minutes = ?");
3745
4333
  params.push(input.estimated_minutes);
3746
4334
  }
4335
+ if (input.sla_minutes !== undefined) {
4336
+ sets.push("sla_minutes = ?");
4337
+ params.push(input.sla_minutes);
4338
+ }
3747
4339
  if (input.actual_minutes !== undefined) {
3748
4340
  sets.push("actual_minutes = ?");
3749
4341
  params.push(input.actual_minutes);
@@ -3825,6 +4417,7 @@ function updateTask(id, input, db) {
3825
4417
  version: task.version + 1,
3826
4418
  updated_at: timestamp,
3827
4419
  completed_at: input.status === "completed" ? completionTimestamp : input.completed_at !== undefined ? input.completed_at : task.completed_at,
4420
+ sla_minutes: input.sla_minutes !== undefined ? input.sla_minutes : task.sla_minutes,
3828
4421
  actual_minutes: input.actual_minutes ?? task.actual_minutes,
3829
4422
  confidence: input.confidence !== undefined ? input.confidence : task.confidence,
3830
4423
  retry_count: input.retry_count ?? task.retry_count,
@@ -4608,7 +5201,7 @@ function completeTask(id, agentId, db, options) {
4608
5201
  emitLocalEventHooksQuiet({ type: "task.completed", payload: { id, agent_id: agentId, title: task.title, completed_at: timestamp } });
4609
5202
  let spawnedTask = null;
4610
5203
  if (task.recurrence_rule && !options?.skip_recurrence) {
4611
- spawnedTask = spawnNextRecurrence(task, d);
5204
+ spawnedTask = spawnNextRecurrence(task, d, timestamp);
4612
5205
  }
4613
5206
  let spawnedFromTemplate = null;
4614
5207
  if (task.spawns_template_id) {
@@ -4938,8 +5531,9 @@ function claimOrSteal(agentId, filters, db) {
4938
5531
  });
4939
5532
  return tx();
4940
5533
  }
4941
- function spawnNextRecurrence(completedTask, db) {
4942
- const dueAt = nextOccurrence(completedTask.recurrence_rule, new Date);
5534
+ function spawnNextRecurrence(completedTask, db, completedAt) {
5535
+ const recurrenceBase = completedTask.due_at ? new Date(completedTask.due_at) : new Date(completedAt);
5536
+ const dueAt = nextOccurrence(completedTask.recurrence_rule, recurrenceBase);
4943
5537
  let title = completedTask.title;
4944
5538
  if (completedTask.short_id && title.startsWith(completedTask.short_id + ": ")) {
4945
5539
  title = title.slice(completedTask.short_id.length + 2);
@@ -4956,6 +5550,7 @@ function spawnNextRecurrence(completedTask, db) {
4956
5550
  tags: completedTask.tags,
4957
5551
  metadata: completedTask.metadata,
4958
5552
  estimated_minutes: completedTask.estimated_minutes ?? undefined,
5553
+ sla_minutes: completedTask.sla_minutes ?? undefined,
4959
5554
  recurrence_rule: completedTask.recurrence_rule,
4960
5555
  recurrence_parent_id: recurrenceParentId,
4961
5556
  due_at: dueAt
@@ -5208,10 +5803,10 @@ function unarchiveTask(id, db) {
5208
5803
  d.run("UPDATE tasks SET archived_at = NULL WHERE id = ?", [id]);
5209
5804
  return getTask(id, d);
5210
5805
  }
5211
- function getOverdueTasks(projectId, db) {
5806
+ function getOverdueTasks(projectId, db, at = new Date) {
5212
5807
  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')`;
5808
+ const nowStr = at.toISOString();
5809
+ 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
5810
  const params = [nowStr];
5216
5811
  if (projectId) {
5217
5812
  query += ` AND project_id = ?`;
@@ -5221,20 +5816,297 @@ function getOverdueTasks(projectId, db) {
5221
5816
  const rows = d.query(query).all(...params);
5222
5817
  return rows.map(rowToTask);
5223
5818
  }
5224
- function logCost(taskId, tokens, usd, db) {
5819
+ function getEscalatedTasks(opts = {}, db, at = new Date) {
5225
5820
  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]);
5821
+ const nowMs = at.getTime();
5822
+ const conditions = [
5823
+ "archived_at IS NULL",
5824
+ "status NOT IN ('completed', 'cancelled', 'failed')",
5825
+ "(due_at IS NOT NULL OR sla_minutes IS NOT NULL)"
5826
+ ];
5827
+ const params = [];
5828
+ if (opts.project_id) {
5829
+ conditions.push("project_id = ?");
5830
+ params.push(opts.project_id);
5831
+ }
5832
+ if (opts.agent_id) {
5833
+ conditions.push("assigned_to = ?");
5834
+ params.push(opts.agent_id);
5835
+ }
5836
+ const rows = d.query(`SELECT * FROM tasks WHERE ${conditions.join(" AND ")} ORDER BY due_at ASC, created_at ASC`).all(...params);
5837
+ return rows.map(rowToTask).map((task) => {
5838
+ const reasons = [];
5839
+ const breachedTimes = [];
5840
+ if (task.due_at) {
5841
+ const dueMs = new Date(task.due_at).getTime();
5842
+ if (Number.isFinite(dueMs) && dueMs < nowMs) {
5843
+ reasons.push("overdue");
5844
+ breachedTimes.push(dueMs);
5845
+ }
5846
+ }
5847
+ if (task.sla_minutes != null) {
5848
+ const startMs = new Date(task.started_at ?? task.created_at).getTime();
5849
+ const breachedMs = startMs + task.sla_minutes * 60000;
5850
+ if (Number.isFinite(breachedMs) && breachedMs < nowMs) {
5851
+ reasons.push("sla_breached");
5852
+ breachedTimes.push(breachedMs);
5853
+ }
5854
+ }
5855
+ if (reasons.length === 0)
5856
+ return null;
5857
+ return {
5858
+ task,
5859
+ reasons,
5860
+ breached_at: new Date(Math.min(...breachedTimes)).toISOString()
5861
+ };
5862
+ }).filter((item) => item !== null);
5227
5863
  }
5228
- // src/db/plans.ts
5229
- init_types();
5230
- init_database();
5231
- function createPlan(input, db) {
5864
+ function rowToFocusSession(row) {
5865
+ return {
5866
+ ...row,
5867
+ metadata: JSON.parse(row.metadata || "{}")
5868
+ };
5869
+ }
5870
+ function minutesBetween(start, end) {
5871
+ if (!start || !end)
5872
+ return 0;
5873
+ const startMs = Date.parse(start);
5874
+ const endMs = Date.parse(end);
5875
+ if (!Number.isFinite(startMs) || !Number.isFinite(endMs) || endMs <= startMs)
5876
+ return 0;
5877
+ return Math.max(1, Math.ceil((endMs - startMs) / 60000));
5878
+ }
5879
+ function assertPositiveMinutes(minutes) {
5880
+ if (!Number.isFinite(minutes) || minutes < 1)
5881
+ throw new Error("minutes must be a positive integer");
5882
+ return Math.floor(minutes);
5883
+ }
5884
+ function recalculateTaskActualMinutes(taskId, db) {
5885
+ const row = db.query("SELECT COALESCE(SUM(minutes), 0) as total FROM task_time_logs WHERE task_id = ?").get(taskId);
5886
+ const total = Number(row.total || 0);
5887
+ db.run("UPDATE tasks SET actual_minutes = ?, updated_at = ? WHERE id = ?", [total, now(), taskId]);
5888
+ return total;
5889
+ }
5890
+ function logTime(input, db) {
5232
5891
  const d = db || getDatabase();
5892
+ if (!getTask(input.task_id, d))
5893
+ throw new Error(`Task not found: ${input.task_id}`);
5233
5894
  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 (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
5237
- id,
5895
+ const ts = now();
5896
+ const minutes = assertPositiveMinutes(input.minutes);
5897
+ 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)
5898
+ 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]);
5899
+ recalculateTaskActualMinutes(input.task_id, d);
5900
+ return {
5901
+ id,
5902
+ task_id: input.task_id,
5903
+ run_id: input.run_id || null,
5904
+ focus_session_id: input.focus_session_id || null,
5905
+ agent_id: input.agent_id || null,
5906
+ minutes,
5907
+ started_at: input.started_at || null,
5908
+ ended_at: input.ended_at || null,
5909
+ notes: input.notes || null,
5910
+ created_at: ts
5911
+ };
5912
+ }
5913
+ function getTimeLogs(taskId, db) {
5914
+ const d = db || getDatabase();
5915
+ return d.query(`SELECT * FROM task_time_logs WHERE task_id = ? ORDER BY created_at DESC`).all(taskId);
5916
+ }
5917
+ function startFocusSession(input, db) {
5918
+ const d = db || getDatabase();
5919
+ if (input.task_id && !getTask(input.task_id, d))
5920
+ throw new Error(`Task not found: ${input.task_id}`);
5921
+ const id = uuid();
5922
+ const ts = now();
5923
+ const startedAt = input.started_at || ts;
5924
+ d.run(`INSERT INTO focus_sessions (
5925
+ id, task_id, plan_id, run_id, agent_id, title, status, started_at, last_resumed_at,
5926
+ actual_minutes, idle_after_minutes, notes, metadata, created_at, updated_at
5927
+ ) VALUES (?, ?, ?, ?, ?, ?, 'active', ?, ?, 0, ?, ?, ?, ?, ?)`, [
5928
+ id,
5929
+ input.task_id || null,
5930
+ input.plan_id || null,
5931
+ input.run_id || null,
5932
+ input.agent_id || null,
5933
+ input.title || null,
5934
+ startedAt,
5935
+ startedAt,
5936
+ input.idle_after_minutes ?? null,
5937
+ input.notes || null,
5938
+ JSON.stringify(input.metadata || {}),
5939
+ ts,
5940
+ ts
5941
+ ]);
5942
+ return getFocusSession(id, d);
5943
+ }
5944
+ function getFocusSession(id, db) {
5945
+ const d = db || getDatabase();
5946
+ const row = d.query("SELECT * FROM focus_sessions WHERE id = ?").get(id);
5947
+ return row ? rowToFocusSession(row) : null;
5948
+ }
5949
+ function listFocusSessions(query = {}, db) {
5950
+ const d = db || getDatabase();
5951
+ const conditions = [];
5952
+ const params = [];
5953
+ if (query.task_id) {
5954
+ conditions.push("task_id = ?");
5955
+ params.push(query.task_id);
5956
+ }
5957
+ if (query.plan_id) {
5958
+ conditions.push("plan_id = ?");
5959
+ params.push(query.plan_id);
5960
+ }
5961
+ if (query.run_id) {
5962
+ conditions.push("run_id = ?");
5963
+ params.push(query.run_id);
5964
+ }
5965
+ if (query.agent_id) {
5966
+ conditions.push("agent_id = ?");
5967
+ params.push(query.agent_id);
5968
+ }
5969
+ if (query.status) {
5970
+ conditions.push("status = ?");
5971
+ params.push(query.status);
5972
+ } else if (!query.include_completed) {
5973
+ conditions.push("status IN ('active', 'paused')");
5974
+ }
5975
+ let sql = "SELECT * FROM focus_sessions";
5976
+ if (conditions.length > 0)
5977
+ sql += ` WHERE ${conditions.join(" AND ")}`;
5978
+ sql += " ORDER BY updated_at DESC";
5979
+ if (query.limit) {
5980
+ sql += " LIMIT ?";
5981
+ params.push(query.limit);
5982
+ }
5983
+ return d.query(sql).all(...params).map(rowToFocusSession);
5984
+ }
5985
+ function pauseFocusSession(id, pausedAt, db) {
5986
+ const d = db || getDatabase();
5987
+ const session = getFocusSession(id, d);
5988
+ if (!session)
5989
+ throw new Error(`Focus session not found: ${id}`);
5990
+ if (session.status !== "active")
5991
+ throw new Error(`Focus session is ${session.status}, not active`);
5992
+ const ts = now();
5993
+ const pauseAt = pausedAt || ts;
5994
+ const minutes = session.actual_minutes + minutesBetween(session.last_resumed_at, pauseAt);
5995
+ d.run("UPDATE focus_sessions SET status = 'paused', actual_minutes = ?, paused_at = ?, last_resumed_at = NULL, updated_at = ? WHERE id = ?", [minutes, pauseAt, ts, id]);
5996
+ return getFocusSession(id, d);
5997
+ }
5998
+ function resumeFocusSession(id, resumedAt, db) {
5999
+ const d = db || getDatabase();
6000
+ const session = getFocusSession(id, d);
6001
+ if (!session)
6002
+ throw new Error(`Focus session not found: ${id}`);
6003
+ if (session.status !== "paused")
6004
+ throw new Error(`Focus session is ${session.status}, not paused`);
6005
+ const ts = now();
6006
+ const resumed = resumedAt || ts;
6007
+ d.run("UPDATE focus_sessions SET status = 'active', paused_at = NULL, last_resumed_at = ?, updated_at = ? WHERE id = ?", [resumed, ts, id]);
6008
+ return getFocusSession(id, d);
6009
+ }
6010
+ function stopFocusSession(input, db) {
6011
+ const d = db || getDatabase();
6012
+ const session = getFocusSession(input.id, d);
6013
+ if (!session)
6014
+ throw new Error(`Focus session not found: ${input.id}`);
6015
+ if (session.status === "completed" || session.status === "cancelled")
6016
+ return session;
6017
+ const ts = now();
6018
+ const endedAt = input.ended_at || ts;
6019
+ const finalMinutes = session.status === "active" ? session.actual_minutes + minutesBetween(session.last_resumed_at, endedAt) : session.actual_minutes;
6020
+ const finalStatus = input.status || "completed";
6021
+ 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]);
6022
+ const stopped = getFocusSession(input.id, d);
6023
+ if (stopped.task_id && finalStatus === "completed" && finalMinutes > 0) {
6024
+ logTime({
6025
+ task_id: stopped.task_id,
6026
+ run_id: stopped.run_id || undefined,
6027
+ focus_session_id: stopped.id,
6028
+ agent_id: stopped.agent_id || undefined,
6029
+ minutes: finalMinutes,
6030
+ started_at: stopped.started_at,
6031
+ ended_at: stopped.ended_at || endedAt,
6032
+ notes: input.notes || stopped.notes || undefined
6033
+ }, d);
6034
+ }
6035
+ return stopped;
6036
+ }
6037
+ function getIdleFocusSessionPrompts(opts = {}, db) {
6038
+ const d = db || getDatabase();
6039
+ const nowDate = opts.now ? new Date(opts.now) : new Date;
6040
+ const sessions = listFocusSessions({ agent_id: opts.agent_id, status: "active", include_completed: false }, d);
6041
+ return sessions.flatMap((session) => {
6042
+ if (!session.idle_after_minutes || !session.last_resumed_at)
6043
+ return [];
6044
+ const idleMinutes = Math.floor((nowDate.getTime() - Date.parse(session.last_resumed_at)) / 60000);
6045
+ if (!Number.isFinite(idleMinutes) || idleMinutes < session.idle_after_minutes)
6046
+ return [];
6047
+ return [{
6048
+ session,
6049
+ idle_minutes: idleMinutes,
6050
+ message: `Focus session ${session.id.slice(0, 8)} has been active for ${idleMinutes} minutes. Pause, stop, or continue it.`
6051
+ }];
6052
+ });
6053
+ }
6054
+ function getTimeReport(opts, db) {
6055
+ const d = db || getDatabase();
6056
+ const conditions = opts?.include_open ? ["1 = 1"] : ["t.status = 'completed'"];
6057
+ const params = [];
6058
+ if (opts?.project_id) {
6059
+ conditions.push("t.project_id = ?");
6060
+ params.push(opts.project_id);
6061
+ }
6062
+ if (opts?.plan_id) {
6063
+ conditions.push("t.plan_id = ?");
6064
+ params.push(opts.plan_id);
6065
+ }
6066
+ if (opts?.agent_id) {
6067
+ conditions.push(`(
6068
+ t.assigned_to = ?
6069
+ OR t.agent_id = ?
6070
+ OR EXISTS (SELECT 1 FROM task_time_logs ttl WHERE ttl.task_id = t.id AND ttl.agent_id = ?)
6071
+ OR EXISTS (SELECT 1 FROM focus_sessions fs WHERE fs.task_id = t.id AND fs.agent_id = ?)
6072
+ )`);
6073
+ params.push(opts.agent_id, opts.agent_id, opts.agent_id, opts.agent_id);
6074
+ }
6075
+ if (opts?.since) {
6076
+ conditions.push("(t.completed_at >= ? OR t.updated_at >= ?)");
6077
+ params.push(opts.since, opts.since);
6078
+ }
6079
+ const rows = d.query(`
6080
+ SELECT t.id as task_id, t.title, t.project_id, t.plan_id, t.estimated_minutes, t.actual_minutes
6081
+ FROM tasks t
6082
+ WHERE ${conditions.join(" AND ")}
6083
+ ORDER BY t.completed_at DESC, t.updated_at DESC
6084
+ `).all(...params);
6085
+ return rows.map((row) => {
6086
+ const timeLogs = getTimeLogs(row.task_id, d);
6087
+ const focusSessions = listFocusSessions({ task_id: row.task_id, include_completed: true }, d);
6088
+ const loggedMinutes = timeLogs.reduce((acc, log) => acc + log.minutes, 0);
6089
+ const focusMinutes = focusSessions.reduce((acc, session) => acc + session.actual_minutes, 0);
6090
+ return { ...row, logged_minutes: loggedMinutes, focus_minutes: focusMinutes, time_logs: timeLogs, focus_sessions: focusSessions };
6091
+ });
6092
+ }
6093
+ function logCost(taskId, tokens, usd, db) {
6094
+ const d = db || getDatabase();
6095
+ d.run("UPDATE tasks SET cost_tokens = cost_tokens + ?, cost_usd = cost_usd + ?, updated_at = ? WHERE id = ?", [tokens, usd, now(), taskId]);
6096
+ }
6097
+ // src/db/boards.ts
6098
+ init_database();
6099
+
6100
+ // src/db/plans.ts
6101
+ init_types();
6102
+ init_database();
6103
+ function createPlan(input, db) {
6104
+ const d = db || getDatabase();
6105
+ const id = uuid();
6106
+ const timestamp = now();
6107
+ d.run(`INSERT INTO plans (id, project_id, task_list_id, agent_id, name, description, status, created_at, updated_at)
6108
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
6109
+ id,
5238
6110
  input.project_id || null,
5239
6111
  input.task_list_id || null,
5240
6112
  input.agent_id || null,
@@ -5244,62 +6116,1470 @@ function createPlan(input, db) {
5244
6116
  timestamp,
5245
6117
  timestamp
5246
6118
  ]);
5247
- return getPlan(id, d);
6119
+ return getPlan(id, d);
6120
+ }
6121
+ function getPlan(id, db) {
6122
+ const d = db || getDatabase();
6123
+ const row = d.query("SELECT * FROM plans WHERE id = ?").get(id);
6124
+ return row;
6125
+ }
6126
+ function listPlans(projectId, db) {
6127
+ const d = db || getDatabase();
6128
+ if (projectId) {
6129
+ return d.query("SELECT * FROM plans WHERE project_id = ? ORDER BY created_at DESC").all(projectId);
6130
+ }
6131
+ return d.query("SELECT * FROM plans ORDER BY created_at DESC").all();
6132
+ }
6133
+ function updatePlan(id, input, db) {
6134
+ const d = db || getDatabase();
6135
+ const plan = getPlan(id, d);
6136
+ if (!plan)
6137
+ throw new PlanNotFoundError(id);
6138
+ const sets = ["updated_at = ?"];
6139
+ const params = [now()];
6140
+ if (input.name !== undefined) {
6141
+ sets.push("name = ?");
6142
+ params.push(input.name);
6143
+ }
6144
+ if (input.description !== undefined) {
6145
+ sets.push("description = ?");
6146
+ params.push(input.description);
6147
+ }
6148
+ if (input.status !== undefined) {
6149
+ sets.push("status = ?");
6150
+ params.push(input.status);
6151
+ }
6152
+ if (input.task_list_id !== undefined) {
6153
+ sets.push("task_list_id = ?");
6154
+ params.push(input.task_list_id);
6155
+ }
6156
+ if (input.agent_id !== undefined) {
6157
+ sets.push("agent_id = ?");
6158
+ params.push(input.agent_id);
6159
+ }
6160
+ params.push(id);
6161
+ d.run(`UPDATE plans SET ${sets.join(", ")} WHERE id = ?`, params);
6162
+ const updated = getPlan(id, d);
6163
+ emitLocalEventHooksQuiet({
6164
+ type: "plan.updated",
6165
+ payload: { id, old_status: plan.status, new_status: updated.status, name: updated.name, project_id: updated.project_id }
6166
+ });
6167
+ return updated;
6168
+ }
6169
+ function deletePlan(id, db) {
6170
+ const d = db || getDatabase();
6171
+ const result = d.run("DELETE FROM plans WHERE id = ?", [id]);
6172
+ return result.changes > 0;
6173
+ }
6174
+
6175
+ // src/db/boards.ts
6176
+ var TASK_LANES = [
6177
+ { id: "ready", name: "Ready", statuses: ["pending"], wip_limit: null, position: 0 },
6178
+ { id: "doing", name: "Doing", statuses: ["in_progress"], wip_limit: 3, position: 1 },
6179
+ { id: "review", name: "Review", statuses: ["failed"], wip_limit: 5, position: 2 },
6180
+ { id: "done", name: "Done", statuses: ["completed"], wip_limit: null, position: 3 },
6181
+ { id: "cancelled", name: "Cancelled", statuses: ["cancelled"], wip_limit: null, position: 4 }
6182
+ ];
6183
+ var PLAN_LANES = [
6184
+ { id: "active", name: "Active", statuses: ["active"], wip_limit: 3, position: 0 },
6185
+ { id: "completed", name: "Completed", statuses: ["completed"], wip_limit: null, position: 1 },
6186
+ { id: "archived", name: "Archived", statuses: ["archived"], wip_limit: null, position: 2 }
6187
+ ];
6188
+ function parseJsonObject(value) {
6189
+ if (!value)
6190
+ return {};
6191
+ if (typeof value === "object" && !Array.isArray(value))
6192
+ return value;
6193
+ if (typeof value !== "string")
6194
+ return {};
6195
+ try {
6196
+ const parsed = JSON.parse(value);
6197
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
6198
+ } catch {
6199
+ return {};
6200
+ }
6201
+ }
6202
+ function parseJsonArray(value) {
6203
+ if (Array.isArray(value))
6204
+ return value;
6205
+ if (typeof value !== "string")
6206
+ return [];
6207
+ try {
6208
+ const parsed = JSON.parse(value);
6209
+ return Array.isArray(parsed) ? parsed : [];
6210
+ } catch {
6211
+ return [];
6212
+ }
6213
+ }
6214
+ function defaultLanes(scope) {
6215
+ return (scope === "plans" ? PLAN_LANES : TASK_LANES).map((lane) => ({ ...lane, statuses: [...lane.statuses] }));
6216
+ }
6217
+ function normalizeLanes(scope, lanes) {
6218
+ const source = lanes && lanes.length > 0 ? lanes : defaultLanes(scope);
6219
+ return source.map((lane, index) => ({
6220
+ id: lane.id || lane.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || `lane-${index + 1}`,
6221
+ name: lane.name || lane.id || `Lane ${index + 1}`,
6222
+ statuses: Array.from(new Set((lane.statuses || []).map(String).filter(Boolean))),
6223
+ wip_limit: lane.wip_limit === undefined ? null : lane.wip_limit,
6224
+ position: lane.position ?? index
6225
+ })).filter((lane) => lane.statuses.length > 0).sort((a, b) => a.position - b.position);
6226
+ }
6227
+ function rowToTaskBoard(row) {
6228
+ return {
6229
+ ...row,
6230
+ lanes: normalizeLanes(row.scope, parseJsonArray(row.lanes)),
6231
+ filters: parseJsonObject(row.filters)
6232
+ };
6233
+ }
6234
+ function maybeFilter(value) {
6235
+ return value === undefined || value === null || value === "" ? undefined : value;
6236
+ }
6237
+ function createTaskBoard(input, db) {
6238
+ const d = db || getDatabase();
6239
+ const id = uuid();
6240
+ const timestamp = now();
6241
+ const scope = input.scope || "tasks";
6242
+ const lanes = normalizeLanes(scope, input.lanes);
6243
+ d.run(`INSERT INTO task_boards (id, name, scope, project_id, task_list_id, plan_id, agent_id, lanes, filters, created_at, updated_at)
6244
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
6245
+ id,
6246
+ input.name,
6247
+ scope,
6248
+ input.project_id || null,
6249
+ input.task_list_id || null,
6250
+ input.plan_id || null,
6251
+ input.agent_id || null,
6252
+ JSON.stringify(lanes),
6253
+ JSON.stringify(input.filters || {}),
6254
+ timestamp,
6255
+ timestamp
6256
+ ]);
6257
+ return getTaskBoard(id, d);
6258
+ }
6259
+ function getTaskBoard(idOrName, db) {
6260
+ const d = db || getDatabase();
6261
+ const row = d.query("SELECT * FROM task_boards WHERE id = ? OR name = ?").get(idOrName, idOrName);
6262
+ return row ? rowToTaskBoard(row) : null;
6263
+ }
6264
+ function listTaskBoards(query = {}, db) {
6265
+ const d = db || getDatabase();
6266
+ const conditions = [];
6267
+ const params = [];
6268
+ if (query.scope) {
6269
+ conditions.push("scope = ?");
6270
+ params.push(query.scope);
6271
+ }
6272
+ if (query.project_id) {
6273
+ conditions.push("project_id = ?");
6274
+ params.push(query.project_id);
6275
+ }
6276
+ if (query.task_list_id) {
6277
+ conditions.push("task_list_id = ?");
6278
+ params.push(query.task_list_id);
6279
+ }
6280
+ if (query.plan_id) {
6281
+ conditions.push("plan_id = ?");
6282
+ params.push(query.plan_id);
6283
+ }
6284
+ if (query.agent_id) {
6285
+ conditions.push("agent_id = ?");
6286
+ params.push(query.agent_id);
6287
+ }
6288
+ let sql = "SELECT * FROM task_boards";
6289
+ if (conditions.length > 0)
6290
+ sql += ` WHERE ${conditions.join(" AND ")}`;
6291
+ sql += " ORDER BY updated_at DESC, name";
6292
+ if (query.limit) {
6293
+ sql += " LIMIT ?";
6294
+ params.push(query.limit);
6295
+ }
6296
+ return d.query(sql).all(...params).map(rowToTaskBoard);
6297
+ }
6298
+ function updateTaskBoard(idOrName, input, db) {
6299
+ const d = db || getDatabase();
6300
+ const board = getTaskBoard(idOrName, d);
6301
+ if (!board)
6302
+ throw new Error(`Board not found: ${idOrName}`);
6303
+ const sets = ["updated_at = ?"];
6304
+ const params = [now()];
6305
+ if (input.name !== undefined) {
6306
+ sets.push("name = ?");
6307
+ params.push(input.name);
6308
+ }
6309
+ if (input.project_id !== undefined) {
6310
+ sets.push("project_id = ?");
6311
+ params.push(input.project_id);
6312
+ }
6313
+ if (input.task_list_id !== undefined) {
6314
+ sets.push("task_list_id = ?");
6315
+ params.push(input.task_list_id);
6316
+ }
6317
+ if (input.plan_id !== undefined) {
6318
+ sets.push("plan_id = ?");
6319
+ params.push(input.plan_id);
6320
+ }
6321
+ if (input.agent_id !== undefined) {
6322
+ sets.push("agent_id = ?");
6323
+ params.push(input.agent_id);
6324
+ }
6325
+ if (input.lanes !== undefined) {
6326
+ sets.push("lanes = ?");
6327
+ params.push(JSON.stringify(normalizeLanes(board.scope, input.lanes)));
6328
+ }
6329
+ if (input.filters !== undefined) {
6330
+ sets.push("filters = ?");
6331
+ params.push(JSON.stringify(input.filters));
6332
+ }
6333
+ params.push(board.id);
6334
+ d.run(`UPDATE task_boards SET ${sets.join(", ")} WHERE id = ?`, params);
6335
+ return getTaskBoard(board.id, d);
6336
+ }
6337
+ function deleteTaskBoard(idOrName, db) {
6338
+ const d = db || getDatabase();
6339
+ const board = getTaskBoard(idOrName, d);
6340
+ if (!board)
6341
+ return false;
6342
+ return d.run("DELETE FROM task_boards WHERE id = ?", [board.id]).changes > 0;
6343
+ }
6344
+ function incompleteDependencyCount(taskId, db) {
6345
+ const row = db.query(`SELECT COUNT(*) as count
6346
+ FROM task_dependencies td
6347
+ JOIN tasks dep ON dep.id = td.depends_on
6348
+ WHERE td.task_id = ? AND dep.status NOT IN ('completed', 'cancelled')`).get(taskId);
6349
+ return row.count;
6350
+ }
6351
+ function taskToCard(task, db) {
6352
+ const blockedCount = incompleteDependencyCount(task.id, db);
6353
+ const blocked = blockedCount > 0;
6354
+ const badges = [task.priority];
6355
+ if (blocked)
6356
+ badges.push("blocked");
6357
+ if (!blocked && task.status === "pending")
6358
+ badges.push("ready");
6359
+ if (task.assigned_to)
6360
+ badges.push(`@${task.assigned_to}`);
6361
+ if (task.due_at && Date.parse(task.due_at) < Date.now() && !["completed", "cancelled", "failed"].includes(task.status)) {
6362
+ badges.push("overdue");
6363
+ }
6364
+ return {
6365
+ id: task.id,
6366
+ short_id: task.short_id,
6367
+ title: task.title,
6368
+ status: task.status,
6369
+ priority: task.priority,
6370
+ project_id: task.project_id,
6371
+ plan_id: task.plan_id,
6372
+ task_list_id: task.task_list_id,
6373
+ assigned_to: task.assigned_to,
6374
+ blocked,
6375
+ ready: !blocked && task.status === "pending",
6376
+ badges,
6377
+ updated_at: task.updated_at
6378
+ };
6379
+ }
6380
+ function planTaskStats(planId, db) {
6381
+ const tasks = listTasks({ plan_id: planId, include_archived: true }, db);
6382
+ let blocked = 0;
6383
+ let ready = 0;
6384
+ for (const task of tasks) {
6385
+ const isBlocked = incompleteDependencyCount(task.id, db) > 0;
6386
+ if (isBlocked)
6387
+ blocked++;
6388
+ if (!isBlocked && task.status === "pending")
6389
+ ready++;
6390
+ }
6391
+ return {
6392
+ total: tasks.length,
6393
+ active: tasks.filter((task) => !["completed", "cancelled", "failed"].includes(task.status)).length,
6394
+ blocked,
6395
+ ready
6396
+ };
6397
+ }
6398
+ function planToCard(plan, db) {
6399
+ const stats = planTaskStats(plan.id, db);
6400
+ const badges = [`tasks:${stats.total}`, `active:${stats.active}`];
6401
+ if (stats.blocked > 0)
6402
+ badges.push(`blocked:${stats.blocked}`);
6403
+ if (stats.ready > 0)
6404
+ badges.push(`ready:${stats.ready}`);
6405
+ return {
6406
+ id: plan.id,
6407
+ short_id: null,
6408
+ title: plan.name,
6409
+ status: plan.status,
6410
+ priority: null,
6411
+ project_id: plan.project_id,
6412
+ plan_id: plan.id,
6413
+ task_list_id: plan.task_list_id,
6414
+ assigned_to: plan.agent_id,
6415
+ blocked: stats.blocked > 0,
6416
+ ready: stats.ready > 0,
6417
+ badges,
6418
+ updated_at: plan.updated_at
6419
+ };
6420
+ }
6421
+ function filteredTasks(board, db) {
6422
+ const filters = board.filters;
6423
+ return listTasks({
6424
+ project_id: board.project_id || maybeFilter(filters.project_id),
6425
+ task_list_id: board.task_list_id || maybeFilter(filters.task_list_id),
6426
+ plan_id: board.plan_id || maybeFilter(filters.plan_id),
6427
+ assigned_to: board.agent_id || maybeFilter(filters.assigned_to),
6428
+ agent_id: maybeFilter(filters.agent_id),
6429
+ priority: maybeFilter(filters.priority),
6430
+ tags: Array.isArray(filters.tags) ? filters.tags.map(String) : undefined,
6431
+ task_type: maybeFilter(filters.task_type),
6432
+ include_archived: Boolean(filters.include_archived)
6433
+ }, db);
6434
+ }
6435
+ function filteredPlans(board, db) {
6436
+ 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);
6437
+ }
6438
+ function buildLaneSnapshots(board, cards) {
6439
+ return board.lanes.map((lane) => {
6440
+ const laneCards = cards.filter((card) => lane.statuses.includes(card.status));
6441
+ return {
6442
+ lane,
6443
+ count: laneCards.length,
6444
+ wip_limit: lane.wip_limit,
6445
+ wip_exceeded: lane.wip_limit !== null && laneCards.length > lane.wip_limit,
6446
+ cards: laneCards
6447
+ };
6448
+ });
6449
+ }
6450
+ function buildTaskBoardSnapshot(idOrBoard, db) {
6451
+ const d = db || getDatabase();
6452
+ const board = typeof idOrBoard === "string" ? getTaskBoard(idOrBoard, d) : idOrBoard;
6453
+ if (!board)
6454
+ throw new Error(`Board not found: ${idOrBoard}`);
6455
+ const cards = board.scope === "plans" ? filteredPlans(board, d).map((plan) => planToCard(plan, d)) : filteredTasks(board, d).map((task) => taskToCard(task, d));
6456
+ const lanes = buildLaneSnapshots(board, cards);
6457
+ return {
6458
+ board,
6459
+ generated_at: now(),
6460
+ lanes,
6461
+ totals: {
6462
+ cards: cards.length,
6463
+ blocked: cards.filter((card) => card.blocked).length,
6464
+ ready: cards.filter((card) => card.ready).length,
6465
+ wip_exceeded_lanes: lanes.filter((lane) => lane.wip_exceeded).length
6466
+ },
6467
+ keyboard: {
6468
+ move_left: "h",
6469
+ move_right: "l",
6470
+ move_up: "k",
6471
+ move_down: "j",
6472
+ open: "enter",
6473
+ quit: "q"
6474
+ }
6475
+ };
6476
+ }
6477
+ function moveBoardCard(input, db) {
6478
+ const d = db || getDatabase();
6479
+ const board = getTaskBoard(input.board_id, d);
6480
+ if (!board)
6481
+ throw new Error(`Board not found: ${input.board_id}`);
6482
+ const lane = input.lane_id ? board.lanes.find((candidate) => candidate.id === input.lane_id || candidate.name === input.lane_id) : null;
6483
+ const status = input.status || lane?.statuses[0];
6484
+ if (!status)
6485
+ throw new Error("Target lane or status is required");
6486
+ if (board.scope === "plans") {
6487
+ const planId = resolvePartialId(d, "plans", input.card_id) || input.card_id;
6488
+ const plan = updatePlan(planId, { status }, d);
6489
+ return planToCard(plan, d);
6490
+ }
6491
+ const taskId = resolvePartialId(d, "tasks", input.card_id) || input.card_id;
6492
+ const task = getTask(taskId, d);
6493
+ if (!task)
6494
+ throw new Error(`Task not found: ${input.card_id}`);
6495
+ const updated = updateTask(task.id, { status, version: task.version }, d);
6496
+ return taskToCard(updated, d);
6497
+ }
6498
+ function renderTaskBoard(snapshot) {
6499
+ const lines = [
6500
+ `${snapshot.board.name} (${snapshot.board.scope})`,
6501
+ `cards:${snapshot.totals.cards} ready:${snapshot.totals.ready} blocked:${snapshot.totals.blocked} wip_exceeded:${snapshot.totals.wip_exceeded_lanes}`,
6502
+ `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`,
6503
+ ""
6504
+ ];
6505
+ for (const lane of snapshot.lanes) {
6506
+ const limit = lane.wip_limit === null ? "" : ` / ${lane.wip_limit}`;
6507
+ const marker = lane.wip_exceeded ? " !" : "";
6508
+ lines.push(`${lane.lane.name} (${lane.count}${limit})${marker}`);
6509
+ for (const card of lane.cards) {
6510
+ const id = card.short_id || card.id.slice(0, 8);
6511
+ lines.push(` ${id} ${card.title} [${card.badges.join(", ")}]`);
6512
+ }
6513
+ if (lane.cards.length === 0)
6514
+ lines.push(" (empty)");
6515
+ lines.push("");
6516
+ }
6517
+ return lines.join(`
6518
+ `).trimEnd();
6519
+ }
6520
+ function exportTaskBoardBundle(idOrName, db) {
6521
+ const d = db || getDatabase();
6522
+ const boards = idOrName ? [getTaskBoard(idOrName, d)].filter(Boolean) : listTaskBoards({}, d);
6523
+ return {
6524
+ kind: "hasna.todos.task-board",
6525
+ schemaVersion: 1,
6526
+ exportedAt: now(),
6527
+ boards
6528
+ };
6529
+ }
6530
+ function importTaskBoardBundle(bundle, db) {
6531
+ const d = db || getDatabase();
6532
+ if (bundle.kind !== "hasna.todos.task-board" || bundle.schemaVersion !== 1 || !Array.isArray(bundle.boards)) {
6533
+ throw new Error("Invalid task board bundle");
6534
+ }
6535
+ let inserted = 0;
6536
+ let updated = 0;
6537
+ let skipped = 0;
6538
+ for (const board of bundle.boards) {
6539
+ const existing = getTaskBoard(board.id, d) || getTaskBoard(board.name, d);
6540
+ if (existing) {
6541
+ updateTaskBoard(existing.id, {
6542
+ name: board.name,
6543
+ project_id: board.project_id,
6544
+ task_list_id: board.task_list_id,
6545
+ plan_id: board.plan_id,
6546
+ agent_id: board.agent_id,
6547
+ lanes: board.lanes,
6548
+ filters: board.filters
6549
+ }, d);
6550
+ updated++;
6551
+ } else if (board.name && board.scope) {
6552
+ createTaskBoard({
6553
+ name: board.name,
6554
+ scope: board.scope,
6555
+ project_id: board.project_id || undefined,
6556
+ task_list_id: board.task_list_id || undefined,
6557
+ plan_id: board.plan_id || undefined,
6558
+ agent_id: board.agent_id || undefined,
6559
+ lanes: board.lanes,
6560
+ filters: board.filters
6561
+ }, d);
6562
+ inserted++;
6563
+ } else {
6564
+ skipped++;
6565
+ }
6566
+ }
6567
+ return { inserted, updated, skipped };
6568
+ }
6569
+ // src/db/calendar.ts
6570
+ init_database();
6571
+
6572
+ // src/lib/artifact-store.ts
6573
+ init_database();
6574
+ import { createHash as createHash2 } from "crypto";
6575
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
6576
+ import { dirname as dirname4, join as join4, resolve as resolve6 } from "path";
6577
+ import { tmpdir } from "os";
6578
+ function isInMemoryDb2(path) {
6579
+ return path === ":memory:" || path.startsWith("file::memory:");
6580
+ }
6581
+ function artifactStoreRoot() {
6582
+ if (process.env["HASNA_TODOS_ARTIFACTS_DIR"])
6583
+ return resolve6(process.env["HASNA_TODOS_ARTIFACTS_DIR"]);
6584
+ if (process.env["TODOS_ARTIFACTS_DIR"])
6585
+ return resolve6(process.env["TODOS_ARTIFACTS_DIR"]);
6586
+ const dbPath = getDatabasePath();
6587
+ if (isInMemoryDb2(dbPath))
6588
+ return join4(tmpdir(), "hasna-todos-artifacts");
6589
+ return join4(dirname4(resolve6(dbPath)), "artifacts");
6590
+ }
6591
+ function artifactStorePath(relativePath) {
6592
+ const normalized = relativePath.replace(/\\/g, "/");
6593
+ if (normalized.includes("..") || normalized.startsWith("/") || normalized.length === 0) {
6594
+ throw new Error("Invalid artifact store path");
6595
+ }
6596
+ return join4(artifactStoreRoot(), normalized);
6597
+ }
6598
+ function sha256(buffer) {
6599
+ return createHash2("sha256").update(buffer).digest("hex");
6600
+ }
6601
+ function isTextLike(buffer, path) {
6602
+ if (buffer.includes(0))
6603
+ return false;
6604
+ if (/\.(txt|log|json|jsonl|md|csv|yaml|yml|xml|html|css|js|ts|tsx|jsx|patch|diff)$/i.test(path))
6605
+ return true;
6606
+ const sample = buffer.subarray(0, Math.min(buffer.length, 4096)).toString("utf8");
6607
+ return !sample.includes("\uFFFD");
6608
+ }
6609
+ function retentionExpiresAt(createdAt, retentionDays) {
6610
+ if (retentionDays === undefined)
6611
+ return null;
6612
+ if (!Number.isFinite(retentionDays) || retentionDays < 0)
6613
+ throw new Error("retention_days must be a non-negative number");
6614
+ const expires = new Date(createdAt);
6615
+ expires.setUTCDate(expires.getUTCDate() + retentionDays);
6616
+ return expires.toISOString();
6617
+ }
6618
+ function mediaTypeFor(path, textLike) {
6619
+ if (/\.(png)$/i.test(path))
6620
+ return "image/png";
6621
+ if (/\.(jpe?g)$/i.test(path))
6622
+ return "image/jpeg";
6623
+ if (/\.(gif)$/i.test(path))
6624
+ return "image/gif";
6625
+ if (/\.(webp)$/i.test(path))
6626
+ return "image/webp";
6627
+ if (/\.(json)$/i.test(path))
6628
+ return "application/json";
6629
+ if (/\.(md)$/i.test(path))
6630
+ return "text/markdown";
6631
+ if (textLike)
6632
+ return "text/plain";
6633
+ return "application/octet-stream";
6634
+ }
6635
+ function storeArtifactContent(input) {
6636
+ const sourcePath = resolve6(input.path);
6637
+ if (!existsSync5(sourcePath))
6638
+ return null;
6639
+ const sourceStat = statSync2(sourcePath);
6640
+ if (!sourceStat.isFile())
6641
+ throw new Error(`Artifact path is not a file: ${input.path}`);
6642
+ const sourceBuffer = readFileSync2(sourcePath);
6643
+ const sourceSha = sha256(sourceBuffer);
6644
+ const textLike = isTextLike(sourceBuffer, input.path);
6645
+ let storedBuffer = sourceBuffer;
6646
+ let redactionStatus = textLike ? "clean" : "binary_or_unknown";
6647
+ if (textLike) {
6648
+ const redactedText = redactEvidenceText(sourceBuffer.toString("utf8"));
6649
+ storedBuffer = Buffer.from(redactedText);
6650
+ if (!storedBuffer.equals(sourceBuffer))
6651
+ redactionStatus = "redacted";
6652
+ }
6653
+ const storedSha = sha256(storedBuffer);
6654
+ const relativePath = join4("sha256", storedSha.slice(0, 2), storedSha).replace(/\\/g, "/");
6655
+ const destination = artifactStorePath(relativePath);
6656
+ if (!existsSync5(destination)) {
6657
+ mkdirSync4(dirname4(destination), { recursive: true });
6658
+ writeFileSync2(destination, storedBuffer);
6659
+ }
6660
+ const createdAt = input.created_at || new Date().toISOString();
6661
+ const retentionDays = input.retention_days ?? null;
6662
+ return {
6663
+ size_bytes: storedBuffer.length,
6664
+ sha256: storedSha,
6665
+ store: {
6666
+ stored: true,
6667
+ algorithm: "sha256",
6668
+ sha256: storedSha,
6669
+ size_bytes: storedBuffer.length,
6670
+ relative_path: relativePath,
6671
+ content_address: `sha256:${storedSha}`,
6672
+ media_type: mediaTypeFor(input.path, textLike),
6673
+ redaction: {
6674
+ checked: textLike,
6675
+ status: redactionStatus
6676
+ },
6677
+ retention: {
6678
+ days: retentionDays,
6679
+ expires_at: retentionDays === null ? null : retentionExpiresAt(createdAt, retentionDays)
6680
+ },
6681
+ source: {
6682
+ path: input.path,
6683
+ size_bytes: sourceBuffer.length,
6684
+ sha256: sourceSha
6685
+ }
6686
+ }
6687
+ };
6688
+ }
6689
+ function storeMetadata(metadata) {
6690
+ const value = metadata["artifact_store"];
6691
+ if (!value || typeof value !== "object" || Array.isArray(value))
6692
+ return null;
6693
+ const record = value;
6694
+ if (record.stored !== true)
6695
+ return null;
6696
+ if (typeof record.sha256 !== "string" || typeof record.relative_path !== "string" || typeof record.size_bytes !== "number")
6697
+ return null;
6698
+ return record;
6699
+ }
6700
+ function verifyStoredArtifact(input) {
6701
+ const store = storeMetadata(input.metadata);
6702
+ if (!store) {
6703
+ return {
6704
+ id: input.id,
6705
+ path: input.path,
6706
+ status: "metadata_only",
6707
+ expected_sha256: input.sha256,
6708
+ actual_sha256: null,
6709
+ expected_size_bytes: input.size_bytes,
6710
+ actual_size_bytes: null,
6711
+ relative_path: null,
6712
+ message: "artifact has metadata only and no local stored content"
6713
+ };
6714
+ }
6715
+ const storedPath = artifactStorePath(store.relative_path);
6716
+ if (!existsSync5(storedPath)) {
6717
+ return {
6718
+ id: input.id,
6719
+ path: input.path,
6720
+ status: "missing",
6721
+ expected_sha256: store.sha256,
6722
+ actual_sha256: null,
6723
+ expected_size_bytes: store.size_bytes,
6724
+ actual_size_bytes: null,
6725
+ relative_path: store.relative_path,
6726
+ message: "stored artifact content is missing"
6727
+ };
6728
+ }
6729
+ const buffer = readFileSync2(storedPath);
6730
+ const actualSha = sha256(buffer);
6731
+ const actualSize = buffer.length;
6732
+ const ok = actualSha === store.sha256 && actualSize === store.size_bytes;
6733
+ return {
6734
+ id: input.id,
6735
+ path: input.path,
6736
+ status: ok ? "ok" : "mismatch",
6737
+ expected_sha256: store.sha256,
6738
+ actual_sha256: actualSha,
6739
+ expected_size_bytes: store.size_bytes,
6740
+ actual_size_bytes: actualSize,
6741
+ relative_path: store.relative_path,
6742
+ message: ok ? "stored artifact content matches metadata" : "stored artifact content does not match metadata"
6743
+ };
6744
+ }
6745
+ function exportStoredArtifactContent(input) {
6746
+ const report = verifyStoredArtifact(input);
6747
+ if (report.status !== "ok" || !report.relative_path || !report.actual_sha256 || report.actual_size_bytes === null)
6748
+ return null;
6749
+ const content = readFileSync2(artifactStorePath(report.relative_path));
6750
+ return {
6751
+ artifact_id: input.id,
6752
+ sha256: report.actual_sha256,
6753
+ size_bytes: report.actual_size_bytes,
6754
+ relative_path: report.relative_path,
6755
+ base64: content.toString("base64")
6756
+ };
6757
+ }
6758
+ function importStoredArtifactContent(content) {
6759
+ const buffer = Buffer.from(content.base64, "base64");
6760
+ const actualSha = sha256(buffer);
6761
+ if (actualSha !== content.sha256 || buffer.length !== content.size_bytes) {
6762
+ return {
6763
+ id: content.artifact_id,
6764
+ path: content.relative_path,
6765
+ status: "mismatch",
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: "exported artifact content checksum does not match manifest"
6772
+ };
6773
+ }
6774
+ const destination = artifactStorePath(content.relative_path);
6775
+ mkdirSync4(dirname4(destination), { recursive: true });
6776
+ writeFileSync2(destination, buffer);
6777
+ return {
6778
+ id: content.artifact_id,
6779
+ path: content.relative_path,
6780
+ status: "ok",
6781
+ expected_sha256: content.sha256,
6782
+ actual_sha256: actualSha,
6783
+ expected_size_bytes: content.size_bytes,
6784
+ actual_size_bytes: buffer.length,
6785
+ relative_path: content.relative_path,
6786
+ message: "stored artifact content imported"
6787
+ };
6788
+ }
6789
+
6790
+ // src/db/task-runs.ts
6791
+ init_types();
6792
+
6793
+ // src/db/comments.ts
6794
+ init_types();
6795
+ init_database();
6796
+ function addComment(input, db) {
6797
+ const d = db || getDatabase();
6798
+ if (!getTask(input.task_id, d)) {
6799
+ throw new TaskNotFoundError(input.task_id);
6800
+ }
6801
+ const id = uuid();
6802
+ const timestamp = now();
6803
+ d.run(`INSERT INTO task_comments (id, task_id, agent_id, session_id, content, type, progress_pct, created_at)
6804
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
6805
+ id,
6806
+ input.task_id,
6807
+ input.agent_id || null,
6808
+ input.session_id || null,
6809
+ redactEvidenceText(input.content),
6810
+ input.type || "comment",
6811
+ input.progress_pct ?? null,
6812
+ timestamp
6813
+ ]);
6814
+ return getComment(id, d);
6815
+ }
6816
+ function logProgress(taskId, message, pct, agentId, db) {
6817
+ return addComment({ task_id: taskId, content: message, type: "progress", progress_pct: pct, agent_id: agentId }, db);
6818
+ }
6819
+ function getComment(id, db) {
6820
+ const d = db || getDatabase();
6821
+ return d.query("SELECT * FROM task_comments WHERE id = ?").get(id);
6822
+ }
6823
+ function listComments(taskId, db) {
6824
+ const d = db || getDatabase();
6825
+ return d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(taskId);
6826
+ }
6827
+ function deleteComment(id, db) {
6828
+ const d = db || getDatabase();
6829
+ const result = d.run("DELETE FROM task_comments WHERE id = ?", [id]);
6830
+ return result.changes > 0;
6831
+ }
6832
+
6833
+ // src/db/task-runs.ts
6834
+ init_database();
6835
+
6836
+ // src/db/task-files.ts
6837
+ init_database();
6838
+ function addTaskFile(input, db) {
6839
+ const d = db || getDatabase();
6840
+ const id = uuid();
6841
+ const timestamp = now();
6842
+ const existing = d.query("SELECT id FROM task_files WHERE task_id = ? AND path = ?").get(input.task_id, input.path);
6843
+ if (existing) {
6844
+ 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]);
6845
+ return getTaskFile(existing.id, d);
6846
+ }
6847
+ d.run(`INSERT INTO task_files (id, task_id, path, status, agent_id, note, created_at, updated_at)
6848
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, input.task_id, input.path, input.status || "active", input.agent_id || null, input.note || null, timestamp, timestamp]);
6849
+ return getTaskFile(id, d);
6850
+ }
6851
+ function getTaskFile(id, db) {
6852
+ const d = db || getDatabase();
6853
+ return d.query("SELECT * FROM task_files WHERE id = ?").get(id);
6854
+ }
6855
+ function listTaskFiles(taskId, db) {
6856
+ const d = db || getDatabase();
6857
+ return d.query("SELECT * FROM task_files WHERE task_id = ? ORDER BY path").all(taskId);
6858
+ }
6859
+ function findTasksByFile(path, db) {
6860
+ const d = db || getDatabase();
6861
+ return d.query("SELECT * FROM task_files WHERE path = ? AND status != 'removed' ORDER BY updated_at DESC").all(path);
6862
+ }
6863
+ function updateTaskFileStatus(taskId, path, status, agentId, db) {
6864
+ const d = db || getDatabase();
6865
+ const timestamp = now();
6866
+ 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]);
6867
+ const row = d.query("SELECT * FROM task_files WHERE task_id = ? AND path = ?").get(taskId, path);
6868
+ return row;
6869
+ }
6870
+ function removeTaskFile(taskId, path, db) {
6871
+ const d = db || getDatabase();
6872
+ const result = d.run("DELETE FROM task_files WHERE task_id = ? AND path = ?", [taskId, path]);
6873
+ return result.changes > 0;
6874
+ }
6875
+ function bulkAddTaskFiles(taskId, paths, agentId, db) {
6876
+ const d = db || getDatabase();
6877
+ const results = [];
6878
+ const tx = d.transaction(() => {
6879
+ for (const path of paths) {
6880
+ results.push(addTaskFile({ task_id: taskId, path, agent_id: agentId }, d));
6881
+ }
6882
+ });
6883
+ tx();
6884
+ return results;
6885
+ }
6886
+
6887
+ // src/db/task-commits.ts
6888
+ init_database();
6889
+ function rowToCommit(row) {
6890
+ return {
6891
+ ...row,
6892
+ files_changed: row.files_changed ? JSON.parse(row.files_changed) : null
6893
+ };
6894
+ }
6895
+ function rowToGitRef(row) {
6896
+ return {
6897
+ ...row,
6898
+ metadata: row.metadata ? JSON.parse(row.metadata) : {}
6899
+ };
6900
+ }
6901
+ function rowToVerification(row) {
6902
+ return row;
6903
+ }
6904
+ function getTaskCommits(taskId, db) {
6905
+ const d = db || getDatabase();
6906
+ return d.query("SELECT * FROM task_commits WHERE task_id = ? ORDER BY committed_at DESC, created_at DESC").all(taskId).map(rowToCommit);
6907
+ }
6908
+ function getTaskGitRefs(taskId, db) {
6909
+ const d = db || getDatabase();
6910
+ return d.query("SELECT * FROM task_git_refs WHERE task_id = ? ORDER BY ref_type, updated_at DESC").all(taskId).map(rowToGitRef);
6911
+ }
6912
+ function addTaskVerification(input, db) {
6913
+ const d = db || getDatabase();
6914
+ const id = uuid();
6915
+ const runAt = input.run_at || now();
6916
+ d.run("INSERT INTO task_verifications (id, task_id, command, status, output_summary, artifact_path, agent_id, run_at, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", [
6917
+ id,
6918
+ input.task_id,
6919
+ input.command,
6920
+ input.status || "unknown",
6921
+ input.output_summary ?? null,
6922
+ input.artifact_path ?? null,
6923
+ input.agent_id ?? null,
6924
+ runAt,
6925
+ now()
6926
+ ]);
6927
+ return rowToVerification(d.query("SELECT * FROM task_verifications WHERE id = ?").get(id));
5248
6928
  }
5249
- function getPlan(id, db) {
6929
+ function getTaskVerifications(taskId, db) {
5250
6930
  const d = db || getDatabase();
5251
- const row = d.query("SELECT * FROM plans WHERE id = ?").get(id);
5252
- return row;
6931
+ return d.query("SELECT * FROM task_verifications WHERE task_id = ? ORDER BY run_at DESC, created_at DESC").all(taskId).map(rowToVerification);
5253
6932
  }
5254
- function listPlans(projectId, db) {
6933
+ function getTaskTraceability(taskId, db) {
5255
6934
  const d = db || getDatabase();
5256
- if (projectId) {
5257
- return d.query("SELECT * FROM plans WHERE project_id = ? ORDER BY created_at DESC").all(projectId);
6935
+ return {
6936
+ task_id: taskId,
6937
+ commits: getTaskCommits(taskId, d),
6938
+ git_refs: getTaskGitRefs(taskId, d),
6939
+ verifications: getTaskVerifications(taskId, d)
6940
+ };
6941
+ }
6942
+
6943
+ // src/db/task-runs.ts
6944
+ function parseObject(value) {
6945
+ if (!value)
6946
+ return {};
6947
+ try {
6948
+ const parsed = JSON.parse(value);
6949
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
6950
+ } catch {
6951
+ return {};
5258
6952
  }
5259
- return d.query("SELECT * FROM plans ORDER BY created_at DESC").all();
5260
6953
  }
5261
- function updatePlan(id, input, db) {
6954
+ function rowToRun(row) {
6955
+ return { ...row, metadata: parseObject(row.metadata) };
6956
+ }
6957
+ function rowToEvent(row) {
6958
+ return { ...row, data: parseObject(row.data) };
6959
+ }
6960
+ function rowToArtifact(row) {
6961
+ return { ...row, metadata: parseObject(row.metadata) };
6962
+ }
6963
+ function getRunRow(runId, db) {
6964
+ return db.query("SELECT * FROM task_runs WHERE id = ?").get(runId);
6965
+ }
6966
+ function resolveTaskRunId(idOrPrefix, db) {
5262
6967
  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);
6968
+ const rows = d.query("SELECT id FROM task_runs WHERE id = ? OR id LIKE ? ORDER BY created_at DESC LIMIT 2").all(idOrPrefix, `${idOrPrefix}%`);
6969
+ if (rows.length === 0)
6970
+ throw new Error(`Run not found: ${idOrPrefix}`);
6971
+ if (rows.length > 1)
6972
+ throw new Error(`Run ID is ambiguous: ${idOrPrefix}`);
6973
+ return rows[0].id;
6974
+ }
6975
+ function getTaskRun(runId, db) {
6976
+ const d = db || getDatabase();
6977
+ const row = getRunRow(runId, d);
6978
+ return row ? rowToRun(row) : null;
6979
+ }
6980
+ function startTaskRun(input, db) {
6981
+ const d = db || getDatabase();
6982
+ if (!getTask(input.task_id, d))
6983
+ throw new TaskNotFoundError(input.task_id);
6984
+ const id = uuid();
6985
+ const timestamp = input.started_at || now();
6986
+ if (input.claim && input.agent_id) {
6987
+ startTask(input.task_id, input.agent_id, d);
5279
6988
  }
5280
- if (input.task_list_id !== undefined) {
5281
- sets.push("task_list_id = ?");
5282
- params.push(input.task_list_id);
6989
+ d.run("INSERT INTO task_runs (id, task_id, agent_id, title, status, summary, metadata, started_at, created_at, updated_at) VALUES (?, ?, ?, ?, 'running', ?, ?, ?, ?, ?)", [
6990
+ id,
6991
+ input.task_id,
6992
+ input.agent_id ?? null,
6993
+ input.title ? redactEvidenceText(input.title) : null,
6994
+ input.summary ? redactEvidenceText(input.summary) : null,
6995
+ JSON.stringify(redactValue(input.metadata || {})),
6996
+ timestamp,
6997
+ timestamp,
6998
+ timestamp
6999
+ ]);
7000
+ addTaskRunEvent({
7001
+ run_id: id,
7002
+ event_type: "started",
7003
+ message: input.summary || input.title || "run started",
7004
+ data: { title: input.title, claim: Boolean(input.claim) },
7005
+ agent_id: input.agent_id,
7006
+ created_at: timestamp
7007
+ }, d);
7008
+ if (input.claim && input.agent_id) {
7009
+ addTaskRunEvent({
7010
+ run_id: id,
7011
+ event_type: "claim",
7012
+ message: `claimed by ${input.agent_id}`,
7013
+ data: { agent_id: input.agent_id },
7014
+ agent_id: input.agent_id,
7015
+ created_at: timestamp
7016
+ }, d);
5283
7017
  }
5284
- if (input.agent_id !== undefined) {
5285
- sets.push("agent_id = ?");
5286
- params.push(input.agent_id);
7018
+ const run = getTaskRun(id, d);
7019
+ emitLocalEventHooksQuiet({ type: "run.started", payload: { id: run.id, task_id: run.task_id, agent_id: run.agent_id, title: run.title } });
7020
+ return run;
7021
+ }
7022
+ function addTaskRunEvent(input, db) {
7023
+ const d = db || getDatabase();
7024
+ const runId = resolveTaskRunId(input.run_id, d);
7025
+ const run = getTaskRun(runId, d);
7026
+ if (!run)
7027
+ throw new Error(`Run not found: ${input.run_id}`);
7028
+ const id = uuid();
7029
+ const timestamp = input.created_at || now();
7030
+ d.run("INSERT INTO task_run_events (id, run_id, task_id, event_type, message, data, agent_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [
7031
+ id,
7032
+ run.id,
7033
+ run.task_id,
7034
+ input.event_type,
7035
+ input.message ? redactEvidenceText(input.message) : null,
7036
+ JSON.stringify(redactValue(input.data || {})),
7037
+ input.agent_id ?? run.agent_id,
7038
+ timestamp
7039
+ ]);
7040
+ if (input.event_type === "comment" && input.message) {
7041
+ addComment({ task_id: run.task_id, content: redactEvidenceText(input.message), type: "comment", agent_id: input.agent_id ?? run.agent_id ?? undefined }, d);
5287
7042
  }
5288
- params.push(id);
5289
- d.run(`UPDATE plans SET ${sets.join(", ")} WHERE id = ?`, params);
5290
- const updated = getPlan(id, d);
7043
+ return rowToEvent(d.query("SELECT * FROM task_run_events WHERE id = ?").get(id));
7044
+ }
7045
+ function addTaskRunCommand(input, db) {
7046
+ const d = db || getDatabase();
7047
+ const runId = resolveTaskRunId(input.run_id, d);
7048
+ const run = getTaskRun(runId, d);
7049
+ if (!run)
7050
+ throw new Error(`Run not found: ${input.run_id}`);
7051
+ const id = uuid();
7052
+ const status = input.status || "unknown";
7053
+ const timestamp = now();
7054
+ const command = redactEvidenceText(input.command);
7055
+ const outputSummary = input.output_summary ? redactEvidenceText(input.output_summary) : null;
7056
+ const artifactPath = input.artifact_path ? redactEvidenceText(input.artifact_path) : null;
7057
+ 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
7058
+ id,
7059
+ run.id,
7060
+ run.task_id,
7061
+ command,
7062
+ status,
7063
+ input.exit_code ?? null,
7064
+ outputSummary,
7065
+ artifactPath,
7066
+ input.agent_id ?? run.agent_id,
7067
+ input.started_at ?? null,
7068
+ input.completed_at ?? timestamp,
7069
+ timestamp
7070
+ ]);
7071
+ addTaskVerification({
7072
+ task_id: run.task_id,
7073
+ command,
7074
+ status,
7075
+ output_summary: outputSummary ?? undefined,
7076
+ artifact_path: artifactPath ?? undefined,
7077
+ agent_id: input.agent_id ?? run.agent_id ?? undefined,
7078
+ run_at: input.completed_at ?? timestamp
7079
+ }, d);
7080
+ addTaskRunEvent({
7081
+ run_id: run.id,
7082
+ event_type: "command",
7083
+ message: `${status}: ${command}`,
7084
+ data: {
7085
+ command,
7086
+ status,
7087
+ exit_code: input.exit_code ?? null,
7088
+ output_summary: outputSummary,
7089
+ artifact_path: artifactPath,
7090
+ usage: {
7091
+ tokens: input.tokens ?? null,
7092
+ cost_usd: input.cost_usd ?? null,
7093
+ duration_ms: input.duration_ms ?? null
7094
+ }
7095
+ },
7096
+ agent_id: input.agent_id ?? run.agent_id ?? undefined,
7097
+ created_at: timestamp
7098
+ }, d);
7099
+ return d.query("SELECT * FROM task_run_commands WHERE id = ?").get(id);
7100
+ }
7101
+ function addTaskRunFile(input, db) {
7102
+ const d = db || getDatabase();
7103
+ const runId = resolveTaskRunId(input.run_id, d);
7104
+ const run = getTaskRun(runId, d);
7105
+ if (!run)
7106
+ throw new Error(`Run not found: ${input.run_id}`);
7107
+ const file = addTaskFile({
7108
+ task_id: run.task_id,
7109
+ path: input.path,
7110
+ status: input.status || "modified",
7111
+ note: input.note ? redactEvidenceText(input.note) : undefined,
7112
+ agent_id: input.agent_id ?? run.agent_id ?? undefined
7113
+ }, d);
7114
+ addTaskRunEvent({
7115
+ run_id: run.id,
7116
+ event_type: "file",
7117
+ message: `${file.status}: ${file.path}`,
7118
+ data: { path: file.path, status: file.status, note: file.note },
7119
+ agent_id: input.agent_id ?? run.agent_id ?? undefined
7120
+ }, d);
7121
+ return file;
7122
+ }
7123
+ function addTaskRunArtifact(input, db) {
7124
+ const d = db || getDatabase();
7125
+ const runId = resolveTaskRunId(input.run_id, d);
7126
+ const run = getTaskRun(runId, d);
7127
+ if (!run)
7128
+ throw new Error(`Run not found: ${input.run_id}`);
7129
+ const id = uuid();
7130
+ const timestamp = now();
7131
+ const path = redactEvidenceText(input.path);
7132
+ const description = input.description ? redactEvidenceText(input.description) : null;
7133
+ const metadata = redactValue(input.metadata || {});
7134
+ let sizeBytes = input.size_bytes ?? null;
7135
+ let digest = input.sha256 ?? null;
7136
+ const stored = input.store_content !== false ? storeArtifactContent({ path: input.path, metadata, retention_days: input.retention_days, created_at: timestamp }) : null;
7137
+ if (stored) {
7138
+ sizeBytes = stored.size_bytes;
7139
+ digest = stored.sha256;
7140
+ metadata["artifact_store"] = stored.store;
7141
+ } else if (input.store_content === true) {
7142
+ throw new Error(`Artifact file not found: ${input.path}`);
7143
+ }
7144
+ 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", [
7145
+ id,
7146
+ run.id,
7147
+ run.task_id,
7148
+ path,
7149
+ input.artifact_type ?? null,
7150
+ description,
7151
+ sizeBytes,
7152
+ digest,
7153
+ JSON.stringify(metadata),
7154
+ input.agent_id ?? run.agent_id,
7155
+ timestamp
7156
+ ]);
7157
+ addTaskRunEvent({
7158
+ run_id: run.id,
7159
+ event_type: "artifact",
7160
+ message: description || path,
7161
+ data: { path, artifact_type: input.artifact_type, size_bytes: sizeBytes, sha256: digest, stored: Boolean(stored) },
7162
+ agent_id: input.agent_id ?? run.agent_id ?? undefined,
7163
+ created_at: timestamp
7164
+ }, d);
7165
+ return rowToArtifact(d.query("SELECT * FROM task_run_artifacts WHERE id = ?").get(id));
7166
+ }
7167
+ function verifyTaskRunArtifacts(runId, db) {
7168
+ const ledger = getTaskRunLedger(runId, db);
7169
+ return ledger.artifacts.map((artifact) => verifyStoredArtifact({
7170
+ id: artifact.id,
7171
+ path: artifact.path,
7172
+ size_bytes: artifact.size_bytes,
7173
+ sha256: artifact.sha256,
7174
+ metadata: artifact.metadata
7175
+ }));
7176
+ }
7177
+ function finishTaskRun(input, db) {
7178
+ const d = db || getDatabase();
7179
+ const runId = resolveTaskRunId(input.run_id, d);
7180
+ const run = getTaskRun(runId, d);
7181
+ if (!run)
7182
+ throw new Error(`Run not found: ${input.run_id}`);
7183
+ const timestamp = input.completed_at || now();
7184
+ const summary = input.summary ? redactEvidenceText(input.summary) : null;
7185
+ d.run("UPDATE task_runs SET status = ?, summary = COALESCE(?, summary), completed_at = ?, updated_at = ? WHERE id = ?", [input.status, summary, timestamp, timestamp, run.id]);
7186
+ addTaskRunEvent({
7187
+ run_id: run.id,
7188
+ event_type: input.status,
7189
+ message: summary || `run ${input.status}`,
7190
+ data: { status: input.status },
7191
+ agent_id: input.agent_id ?? run.agent_id ?? undefined,
7192
+ created_at: timestamp
7193
+ }, d);
7194
+ const updated = getTaskRun(run.id, d);
5291
7195
  emitLocalEventHooksQuiet({
5292
- type: "plan.updated",
5293
- payload: { id, old_status: plan.status, new_status: updated.status, name: updated.name, project_id: updated.project_id }
7196
+ type: `run.${input.status}`,
7197
+ payload: { id: updated.id, task_id: updated.task_id, agent_id: updated.agent_id, status: updated.status, summary: updated.summary, completed_at: timestamp }
5294
7198
  });
5295
7199
  return updated;
5296
7200
  }
5297
- function deletePlan(id, db) {
7201
+ function listTaskRuns(taskId, db) {
5298
7202
  const d = db || getDatabase();
5299
- const result = d.run("DELETE FROM plans WHERE id = ?", [id]);
5300
- return result.changes > 0;
7203
+ 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();
7204
+ return rows.map(rowToRun);
7205
+ }
7206
+ function getTaskRunLedger(runId, db) {
7207
+ const d = db || getDatabase();
7208
+ const resolved = resolveTaskRunId(runId, d);
7209
+ const run = getTaskRun(resolved, d);
7210
+ if (!run)
7211
+ throw new Error(`Run not found: ${runId}`);
7212
+ const events = d.query("SELECT * FROM task_run_events WHERE run_id = ? ORDER BY created_at, rowid").all(run.id).map(rowToEvent);
7213
+ const commands = d.query("SELECT * FROM task_run_commands WHERE run_id = ? ORDER BY created_at, rowid").all(run.id);
7214
+ const artifacts = d.query("SELECT * FROM task_run_artifacts WHERE run_id = ? ORDER BY created_at, rowid").all(run.id).map(rowToArtifact);
7215
+ const files = d.query("SELECT * FROM task_files WHERE task_id = ? ORDER BY updated_at DESC, path").all(run.task_id);
7216
+ return { run, events, commands, artifacts, files };
5301
7217
  }
5302
7218
 
7219
+ // src/db/calendar.ts
7220
+ function parseJsonObject2(value) {
7221
+ if (!value)
7222
+ return {};
7223
+ if (typeof value === "object" && !Array.isArray(value))
7224
+ return value;
7225
+ if (typeof value !== "string")
7226
+ return {};
7227
+ try {
7228
+ const parsed = JSON.parse(value);
7229
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
7230
+ } catch {
7231
+ return {};
7232
+ }
7233
+ }
7234
+ function rowToCalendarItem(row) {
7235
+ return { ...row, metadata: parseJsonObject2(row.metadata) };
7236
+ }
7237
+ function eventFromLocal(item) {
7238
+ return {
7239
+ ...item,
7240
+ source: "local",
7241
+ badges: [item.kind, item.timezone || "floating"]
7242
+ };
7243
+ }
7244
+ function addMinutes(iso, minutes) {
7245
+ const ms = Date.parse(iso);
7246
+ if (!Number.isFinite(ms))
7247
+ return iso;
7248
+ return new Date(ms + minutes * 60000).toISOString();
7249
+ }
7250
+ function eventFromTaskDue(task) {
7251
+ if (!task.due_at)
7252
+ return null;
7253
+ return {
7254
+ id: `task-due-${task.id}`,
7255
+ kind: "task_due",
7256
+ title: `Due: ${task.title}`,
7257
+ description: task.description,
7258
+ starts_at: task.due_at,
7259
+ ends_at: null,
7260
+ timezone: null,
7261
+ project_id: task.project_id,
7262
+ task_id: task.id,
7263
+ plan_id: task.plan_id,
7264
+ run_id: null,
7265
+ recurrence_rule: task.recurrence_rule,
7266
+ source: "task",
7267
+ badges: ["due", task.priority, task.status],
7268
+ metadata: { priority: task.priority, status: task.status, short_id: task.short_id }
7269
+ };
7270
+ }
7271
+ function eventFromTaskSla(task) {
7272
+ if (!task.sla_minutes || ["completed", "cancelled", "failed"].includes(task.status))
7273
+ return null;
7274
+ const base = task.started_at || task.created_at;
7275
+ if (!base)
7276
+ return null;
7277
+ const startsAt = addMinutes(base, task.sla_minutes);
7278
+ return {
7279
+ id: `task-sla-${task.id}`,
7280
+ kind: "task_sla",
7281
+ title: `SLA: ${task.title}`,
7282
+ description: task.description,
7283
+ starts_at: startsAt,
7284
+ ends_at: null,
7285
+ timezone: null,
7286
+ project_id: task.project_id,
7287
+ task_id: task.id,
7288
+ plan_id: task.plan_id,
7289
+ run_id: null,
7290
+ recurrence_rule: null,
7291
+ source: "task",
7292
+ badges: ["sla", task.priority, task.status],
7293
+ metadata: { sla_minutes: task.sla_minutes, status: task.status, short_id: task.short_id }
7294
+ };
7295
+ }
7296
+ function eventFromRun(run, task) {
7297
+ return {
7298
+ id: `run-${run.id}`,
7299
+ kind: "run",
7300
+ title: `Run: ${run.title || task?.title || run.id.slice(0, 8)}`,
7301
+ description: run.summary,
7302
+ starts_at: run.started_at,
7303
+ ends_at: run.completed_at,
7304
+ timezone: null,
7305
+ project_id: task?.project_id || null,
7306
+ task_id: run.task_id,
7307
+ plan_id: task?.plan_id || null,
7308
+ run_id: run.id,
7309
+ recurrence_rule: null,
7310
+ source: "run",
7311
+ badges: ["run", run.status],
7312
+ metadata: { status: run.status, agent_id: run.agent_id }
7313
+ };
7314
+ }
7315
+ function inWindow(event, query) {
7316
+ const start = Date.parse(event.starts_at);
7317
+ if (query.from && Number.isFinite(start) && start < Date.parse(query.from))
7318
+ return false;
7319
+ if (query.to && Number.isFinite(start) && start > Date.parse(query.to))
7320
+ return false;
7321
+ if (query.kind && event.kind !== query.kind)
7322
+ return false;
7323
+ if (query.project_id && event.project_id !== query.project_id)
7324
+ return false;
7325
+ if (query.task_id && event.task_id !== query.task_id)
7326
+ return false;
7327
+ if (query.plan_id && event.plan_id !== query.plan_id)
7328
+ return false;
7329
+ if (query.run_id && event.run_id !== query.run_id)
7330
+ return false;
7331
+ return true;
7332
+ }
7333
+ function createCalendarItem(input, db) {
7334
+ const d = db || getDatabase();
7335
+ const id = uuid();
7336
+ const timestamp = now();
7337
+ const kind = input.kind || "work_block";
7338
+ d.run(`INSERT INTO local_calendar_items (
7339
+ id, kind, title, description, starts_at, ends_at, timezone, project_id, task_id, plan_id, run_id,
7340
+ recurrence_rule, metadata, created_at, updated_at
7341
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
7342
+ id,
7343
+ kind,
7344
+ input.title,
7345
+ input.description || null,
7346
+ input.starts_at,
7347
+ input.ends_at || null,
7348
+ input.timezone || null,
7349
+ input.project_id || null,
7350
+ input.task_id || null,
7351
+ input.plan_id || null,
7352
+ input.run_id || null,
7353
+ input.recurrence_rule || null,
7354
+ JSON.stringify(input.metadata || {}),
7355
+ timestamp,
7356
+ timestamp
7357
+ ]);
7358
+ return getCalendarItem(id, d);
7359
+ }
7360
+ function getCalendarItem(id, db) {
7361
+ const d = db || getDatabase();
7362
+ 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;
7363
+ const row = d.query("SELECT * FROM local_calendar_items WHERE id = ?").get(resolved);
7364
+ return row ? rowToCalendarItem(row) : null;
7365
+ }
7366
+ function listCalendarItems(query = {}, db) {
7367
+ const d = db || getDatabase();
7368
+ const conditions = [];
7369
+ const params = [];
7370
+ if (query.project_id) {
7371
+ conditions.push("project_id = ?");
7372
+ params.push(query.project_id);
7373
+ }
7374
+ if (query.task_id) {
7375
+ conditions.push("task_id = ?");
7376
+ params.push(query.task_id);
7377
+ }
7378
+ if (query.plan_id) {
7379
+ conditions.push("plan_id = ?");
7380
+ params.push(query.plan_id);
7381
+ }
7382
+ if (query.run_id) {
7383
+ conditions.push("run_id = ?");
7384
+ params.push(query.run_id);
7385
+ }
7386
+ if (query.kind) {
7387
+ conditions.push("kind = ?");
7388
+ params.push(query.kind);
7389
+ }
7390
+ if (query.from) {
7391
+ conditions.push("starts_at >= ?");
7392
+ params.push(query.from);
7393
+ }
7394
+ if (query.to) {
7395
+ conditions.push("starts_at <= ?");
7396
+ params.push(query.to);
7397
+ }
7398
+ let sql = "SELECT * FROM local_calendar_items";
7399
+ if (conditions.length > 0)
7400
+ sql += ` WHERE ${conditions.join(" AND ")}`;
7401
+ sql += " ORDER BY starts_at ASC, created_at ASC";
7402
+ if (query.limit) {
7403
+ sql += " LIMIT ?";
7404
+ params.push(query.limit);
7405
+ }
7406
+ return d.query(sql).all(...params).map(rowToCalendarItem);
7407
+ }
7408
+ function listCalendarEvents(query = {}, db) {
7409
+ const d = db || getDatabase();
7410
+ const taskFilter = {
7411
+ project_id: query.project_id,
7412
+ plan_id: query.plan_id,
7413
+ ids: query.task_id ? [query.task_id] : undefined,
7414
+ include_archived: true
7415
+ };
7416
+ const tasks = listTasks(taskFilter, d).filter((task) => query.include_completed || !["completed", "cancelled"].includes(task.status));
7417
+ const events = [];
7418
+ for (const task of tasks) {
7419
+ const due = eventFromTaskDue(task);
7420
+ if (due)
7421
+ events.push(due);
7422
+ if (query.include_sla !== false) {
7423
+ const sla = eventFromTaskSla(task);
7424
+ if (sla)
7425
+ events.push(sla);
7426
+ }
7427
+ }
7428
+ if (query.include_runs !== false) {
7429
+ const runs = query.task_id ? listTaskRuns(query.task_id, d) : listTaskRuns(undefined, d);
7430
+ for (const run of runs) {
7431
+ const task = getTask(run.task_id, d);
7432
+ events.push(eventFromRun(run, task));
7433
+ }
7434
+ }
7435
+ if (query.include_local !== false) {
7436
+ events.push(...listCalendarItems(query, d).map(eventFromLocal));
7437
+ }
7438
+ 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);
7439
+ }
7440
+ function pad(value) {
7441
+ return value < 10 ? `0${value}` : String(value);
7442
+ }
7443
+ function icsDate(iso) {
7444
+ const date = new Date(iso);
7445
+ if (Number.isNaN(date.getTime()))
7446
+ return iso.replace(/[-:]/g, "").replace(/\.\d{3}/, "");
7447
+ return `${date.getUTCFullYear()}${pad(date.getUTCMonth() + 1)}${pad(date.getUTCDate())}T${pad(date.getUTCHours())}${pad(date.getUTCMinutes())}${pad(date.getUTCSeconds())}Z`;
7448
+ }
7449
+ function escapeIcs(value) {
7450
+ return String(value || "").replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/,/g, "\\,").replace(/;/g, "\\;");
7451
+ }
7452
+ function foldLine(line) {
7453
+ if (line.length <= 75)
7454
+ return line;
7455
+ const chunks = [];
7456
+ let rest = line;
7457
+ while (rest.length > 75) {
7458
+ chunks.push(rest.slice(0, 75));
7459
+ rest = rest.slice(75);
7460
+ }
7461
+ chunks.push(rest);
7462
+ return chunks.map((chunk, index) => index === 0 ? chunk : ` ${chunk}`).join(`\r
7463
+ `);
7464
+ }
7465
+ function recurrenceToRrule(rule) {
7466
+ if (!rule)
7467
+ return null;
7468
+ const normalized = rule.trim().toLowerCase();
7469
+ if (normalized === "daily" || normalized === "every day")
7470
+ return "FREQ=DAILY";
7471
+ if (normalized === "weekly" || normalized === "every week")
7472
+ return "FREQ=WEEKLY";
7473
+ if (normalized === "monthly" || normalized === "every month")
7474
+ return "FREQ=MONTHLY";
7475
+ if (normalized === "every weekday")
7476
+ return "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR";
7477
+ const everyN = normalized.match(/^every (\d+) (day|days|week|weeks|month|months)$/);
7478
+ if (everyN) {
7479
+ const freq = everyN[2].startsWith("day") ? "DAILY" : everyN[2].startsWith("week") ? "WEEKLY" : "MONTHLY";
7480
+ return `FREQ=${freq};INTERVAL=${everyN[1]}`;
7481
+ }
7482
+ 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))*$/);
7483
+ if (days) {
7484
+ 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" };
7485
+ const byday = normalized.replace("every ", "").split(",").map((day) => map[day] || "").filter(Boolean).join(",");
7486
+ if (byday)
7487
+ return `FREQ=WEEKLY;BYDAY=${byday}`;
7488
+ }
7489
+ if (normalized.startsWith("freq="))
7490
+ return rule.toUpperCase();
7491
+ return null;
7492
+ }
7493
+ function exportCalendarIcs(options = {}, db) {
7494
+ const events = listCalendarEvents(options, db);
7495
+ const generatedAt = options.generated_at || now();
7496
+ const lines = [
7497
+ "BEGIN:VCALENDAR",
7498
+ "VERSION:2.0",
7499
+ `PRODID:${options.product_id || "-//hasna//todos local calendar//EN"}`,
7500
+ "CALSCALE:GREGORIAN",
7501
+ `X-WR-CALNAME:${escapeIcs(options.calendar_name || "Hasna Todos")}`
7502
+ ];
7503
+ for (const event of events) {
7504
+ const title = options.redact ? `${event.kind} ${event.id.slice(0, 8)}` : event.title;
7505
+ const description = options.redact ? null : event.description;
7506
+ lines.push("BEGIN:VEVENT");
7507
+ lines.push(`UID:${escapeIcs(event.id)}@hasna-todos.local`);
7508
+ lines.push(`DTSTAMP:${icsDate(generatedAt)}`);
7509
+ lines.push(`DTSTART:${icsDate(event.starts_at)}`);
7510
+ if (event.ends_at)
7511
+ lines.push(`DTEND:${icsDate(event.ends_at)}`);
7512
+ else
7513
+ lines.push(`DUE:${icsDate(event.starts_at)}`);
7514
+ lines.push(`SUMMARY:${escapeIcs(title)}`);
7515
+ if (description)
7516
+ lines.push(`DESCRIPTION:${escapeIcs(description)}`);
7517
+ lines.push(`CATEGORIES:${escapeIcs(event.badges.join(","))}`);
7518
+ const rrule = recurrenceToRrule(event.recurrence_rule);
7519
+ if (rrule)
7520
+ lines.push(`RRULE:${rrule}`);
7521
+ lines.push(`X-HASNA-TODOS-KIND:${event.kind}`);
7522
+ if (event.task_id)
7523
+ lines.push(`X-HASNA-TODOS-TASK:${event.task_id}`);
7524
+ if (event.plan_id)
7525
+ lines.push(`X-HASNA-TODOS-PLAN:${event.plan_id}`);
7526
+ if (event.run_id)
7527
+ lines.push(`X-HASNA-TODOS-RUN:${event.run_id}`);
7528
+ lines.push("END:VEVENT");
7529
+ }
7530
+ lines.push("END:VCALENDAR");
7531
+ return {
7532
+ filename: "todos-calendar.ics",
7533
+ content: lines.map(foldLine).join(`\r
7534
+ `) + `\r
7535
+ `,
7536
+ events
7537
+ };
7538
+ }
7539
+ function unescapeIcs(value) {
7540
+ return value.replace(/\\n/g, `
7541
+ `).replace(/\\,/g, ",").replace(/\\;/g, ";").replace(/\\\\/g, "\\");
7542
+ }
7543
+ function parseIcsDate(value) {
7544
+ const match = value.match(/^(\d{4})(\d{2})(\d{2})T?(\d{2})?(\d{2})?(\d{2})?Z?$/);
7545
+ if (!match)
7546
+ return value;
7547
+ 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();
7548
+ }
7549
+ function importCalendarIcs(content, db) {
7550
+ const d = db || getDatabase();
7551
+ const unfolded = content.replace(/\r?\n[ \t]/g, "");
7552
+ const blocks = unfolded.match(/BEGIN:VEVENT[\s\S]*?END:VEVENT/g) || [];
7553
+ const items = [];
7554
+ let skipped = 0;
7555
+ for (const block of blocks) {
7556
+ const fields = new Map;
7557
+ for (const rawLine of block.split(/\r?\n/)) {
7558
+ const index = rawLine.indexOf(":");
7559
+ if (index <= 0)
7560
+ continue;
7561
+ const key = rawLine.slice(0, index).split(";")[0].toUpperCase();
7562
+ fields.set(key, rawLine.slice(index + 1));
7563
+ }
7564
+ const title = unescapeIcs(fields.get("SUMMARY") || "");
7565
+ const startsAt = fields.get("DTSTART") || fields.get("DUE");
7566
+ if (!title || !startsAt) {
7567
+ skipped++;
7568
+ continue;
7569
+ }
7570
+ const item = createCalendarItem({
7571
+ kind: "imported",
7572
+ title,
7573
+ description: fields.has("DESCRIPTION") ? unescapeIcs(fields.get("DESCRIPTION")) : undefined,
7574
+ starts_at: parseIcsDate(startsAt),
7575
+ ends_at: fields.has("DTEND") ? parseIcsDate(fields.get("DTEND")) : undefined,
7576
+ recurrence_rule: fields.get("RRULE"),
7577
+ metadata: { uid: fields.get("UID") || null, imported_from: "ics" }
7578
+ }, d);
7579
+ items.push(item);
7580
+ }
7581
+ return { imported: items.length, skipped, items };
7582
+ }
5303
7583
  // src/db/agents.ts
5304
7584
  init_database();
5305
7585
 
@@ -5375,10 +7655,63 @@ var NICE_AGENT_NAMES = [
5375
7655
  "vesper",
5376
7656
  "zephyr"
5377
7657
  ];
7658
+ var EXTENDED_AGENT_NAMES = [
7659
+ "agrippa",
7660
+ "antonius",
7661
+ "aurelian",
7662
+ "aurelius",
7663
+ "camillus",
7664
+ "cassius",
7665
+ "celer",
7666
+ "cincinnatus",
7667
+ "corvus",
7668
+ "drusus",
7669
+ "fabius",
7670
+ "faustus",
7671
+ "flaccus",
7672
+ "gallus",
7673
+ "gaius",
7674
+ "horatius",
7675
+ "lucius",
7676
+ "lucullus",
7677
+ "marius",
7678
+ "marcellus",
7679
+ "maximus",
7680
+ "nerva",
7681
+ "pompey",
7682
+ "quintus",
7683
+ "regulus",
7684
+ "romulus",
7685
+ "scipio",
7686
+ "seneca",
7687
+ "sertorius",
7688
+ "sulla",
7689
+ "tacitus",
7690
+ "varro",
7691
+ "vitruvius",
7692
+ "plato",
7693
+ "socrates",
7694
+ "aristotle",
7695
+ "heraclitus",
7696
+ "democritus",
7697
+ "pythagoras",
7698
+ "hipparchus",
7699
+ "euclid",
7700
+ "archimedes",
7701
+ "zeno",
7702
+ "anaximander",
7703
+ "epictetus",
7704
+ "aeschylus",
7705
+ "sophocles",
7706
+ "euripides",
7707
+ "xenophon",
7708
+ "diogenes"
7709
+ ];
5378
7710
  var PREFERRED_AGENT_NAMES = [
5379
7711
  ...ROMAN_AGENT_NAMES,
5380
7712
  ...GREEK_AGENT_NAMES,
5381
- ...NICE_AGENT_NAMES
7713
+ ...NICE_AGENT_NAMES,
7714
+ ...EXTENDED_AGENT_NAMES
5382
7715
  ];
5383
7716
  var RESERVED_GENERIC_NAMES = new Set([
5384
7717
  "agent",
@@ -5420,29 +7753,93 @@ function isBlockedAgentName(name) {
5420
7753
  const normalized = normalizeAgentNameInput(name);
5421
7754
  return isGenericAgentName(normalized) || hasGeneratedNumericSuffix(normalized) || !ONE_WORD_NAME_RE.test(normalized);
5422
7755
  }
5423
- function alphabeticSuffix(index) {
5424
- const letters = "abcdefghijklmnopqrstuvwxyz";
5425
- let value = index;
5426
- let suffix = "";
5427
- do {
5428
- suffix = letters[value % letters.length] + suffix;
5429
- value = Math.floor(value / letters.length) - 1;
5430
- } while (value >= 0);
5431
- return suffix;
7756
+ var FALLBACK_PREFIXES = [
7757
+ "arv",
7758
+ "bel",
7759
+ "cyr",
7760
+ "dax",
7761
+ "elun",
7762
+ "feno",
7763
+ "gavor",
7764
+ "hiro",
7765
+ "ivar",
7766
+ "jaro",
7767
+ "kavo",
7768
+ "lumo",
7769
+ "myr",
7770
+ "navo",
7771
+ "prax",
7772
+ "quor",
7773
+ "riven",
7774
+ "sovan",
7775
+ "tavor",
7776
+ "ulmor",
7777
+ "vexo",
7778
+ "wiro",
7779
+ "yaro",
7780
+ "zel"
7781
+ ];
7782
+ var FALLBACK_STEMS = [
7783
+ "al",
7784
+ "ber",
7785
+ "cor",
7786
+ "dren",
7787
+ "el",
7788
+ "far",
7789
+ "gor",
7790
+ "hal",
7791
+ "ion",
7792
+ "jor",
7793
+ "kel",
7794
+ "lor",
7795
+ "mor",
7796
+ "nel",
7797
+ "or",
7798
+ "per",
7799
+ "quil",
7800
+ "ron",
7801
+ "ser",
7802
+ "tor",
7803
+ "um",
7804
+ "ver",
7805
+ "wyn",
7806
+ "xil"
7807
+ ];
7808
+ var FALLBACK_ENDINGS = [
7809
+ "a",
7810
+ "en",
7811
+ "ia",
7812
+ "is",
7813
+ "on",
7814
+ "or",
7815
+ "um",
7816
+ "us",
7817
+ "yn",
7818
+ "ar",
7819
+ "el",
7820
+ "ir"
7821
+ ];
7822
+ function generatedFallbackAgentName(index) {
7823
+ const perPrefix = FALLBACK_STEMS.length * FALLBACK_ENDINGS.length;
7824
+ const total = FALLBACK_PREFIXES.length * perPrefix;
7825
+ if (index >= total)
7826
+ return null;
7827
+ const prefix = FALLBACK_PREFIXES[Math.floor(index / perPrefix)];
7828
+ const rest = index % perPrefix;
7829
+ const stem = FALLBACK_STEMS[Math.floor(rest / FALLBACK_ENDINGS.length)];
7830
+ const ending = FALLBACK_ENDINGS[rest % FALLBACK_ENDINGS.length];
7831
+ return `${prefix}${stem}${ending}`;
5432
7832
  }
5433
7833
  function suggestAgentNames(existingNames = []) {
5434
7834
  const existing = new Set([...existingNames].map(normalizeAgentNameInput));
5435
7835
  const suggestions = PREFERRED_AGENT_NAMES.filter((name) => !existing.has(name));
5436
- for (let suffixIndex = 0;suggestions.length < 20 && suffixIndex < 1000; suffixIndex++) {
5437
- const suffix = alphabeticSuffix(suffixIndex);
5438
- for (const base of PREFERRED_AGENT_NAMES) {
5439
- const candidate = `${base}${suffix}`;
5440
- if (existing.has(candidate) || suggestions.includes(candidate))
5441
- continue;
5442
- suggestions.push(candidate);
5443
- if (suggestions.length >= 20)
5444
- break;
5445
- }
7836
+ for (let index = 0;suggestions.length < 20; index++) {
7837
+ const candidate = generatedFallbackAgentName(index);
7838
+ if (!candidate)
7839
+ break;
7840
+ if (existing.has(candidate) || suggestions.includes(candidate))
7841
+ continue;
7842
+ suggestions.push(candidate);
5446
7843
  }
5447
7844
  return suggestions;
5448
7845
  }
@@ -5895,46 +8292,6 @@ function ensureTaskList(name, slug, projectId, db) {
5895
8292
  return createTaskList({ name, slug, project_id: projectId }, d);
5896
8293
  }
5897
8294
 
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
8295
  // src/storage/local-sqlite.ts
5939
8296
  init_database();
5940
8297
  function createLocalSqliteTodosStorageAdapter(options = {}) {