@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.
- package/README.md +695 -11
- package/dist/cli/commands/agent-reliability-commands.d.ts +3 -0
- package/dist/cli/commands/agent-reliability-commands.d.ts.map +1 -0
- package/dist/cli/commands/audit-ledger-commands.d.ts +3 -0
- package/dist/cli/commands/audit-ledger-commands.d.ts.map +1 -0
- package/dist/cli/commands/capacity-commands.d.ts +3 -0
- package/dist/cli/commands/capacity-commands.d.ts.map +1 -0
- package/dist/cli/commands/config-serve-commands.d.ts.map +1 -1
- package/dist/cli/commands/help-commands.d.ts +3 -0
- package/dist/cli/commands/help-commands.d.ts.map +1 -0
- package/dist/cli/commands/knowledge-commands.d.ts +3 -0
- package/dist/cli/commands/knowledge-commands.d.ts.map +1 -0
- package/dist/cli/commands/local-backup-commands.d.ts +3 -0
- package/dist/cli/commands/local-backup-commands.d.ts.map +1 -0
- package/dist/cli/commands/local-snapshot-commands.d.ts +3 -0
- package/dist/cli/commands/local-snapshot-commands.d.ts.map +1 -0
- package/dist/cli/commands/machines.d.ts.map +1 -1
- package/dist/cli/commands/mcp-hooks-commands.d.ts.map +1 -1
- package/dist/cli/commands/onboarding-commands.d.ts +3 -0
- package/dist/cli/commands/onboarding-commands.d.ts.map +1 -0
- package/dist/cli/commands/project-commands.d.ts.map +1 -1
- package/dist/cli/commands/query-commands.d.ts.map +1 -1
- package/dist/cli/commands/release-compatibility-commands.d.ts +3 -0
- package/dist/cli/commands/release-compatibility-commands.d.ts.map +1 -0
- package/dist/cli/commands/retrospective-commands.d.ts +3 -0
- package/dist/cli/commands/retrospective-commands.d.ts.map +1 -0
- package/dist/cli/commands/review-queue-commands.d.ts +3 -0
- package/dist/cli/commands/review-queue-commands.d.ts.map +1 -0
- package/dist/cli/commands/risk-commands.d.ts +3 -0
- package/dist/cli/commands/risk-commands.d.ts.map +1 -0
- package/dist/cli/commands/roadmap-commands.d.ts +3 -0
- package/dist/cli/commands/roadmap-commands.d.ts.map +1 -0
- package/dist/cli/commands/scale-hardening-commands.d.ts +3 -0
- package/dist/cli/commands/scale-hardening-commands.d.ts.map +1 -0
- package/dist/cli/commands/sdk-fixture-commands.d.ts +3 -0
- package/dist/cli/commands/sdk-fixture-commands.d.ts.map +1 -0
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/commands/usage-ledger-commands.d.ts +3 -0
- package/dist/cli/commands/usage-ledger-commands.d.ts.map +1 -0
- package/dist/cli/components/Dashboard.d.ts.map +1 -1
- package/dist/cli/index.js +39446 -19769
- package/dist/cli-mcp-parity.d.ts +1 -1
- package/dist/cli-mcp-parity.d.ts.map +1 -1
- package/dist/contracts.d.ts +25 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +14818 -3885
- package/dist/db/agent-metrics.d.ts +101 -0
- package/dist/db/agent-metrics.d.ts.map +1 -1
- package/dist/db/agent-names.d.ts +2 -1
- package/dist/db/agent-names.d.ts.map +1 -1
- package/dist/db/boards.d.ts +56 -0
- package/dist/db/boards.d.ts.map +1 -0
- package/dist/db/calendar.d.ts +52 -0
- package/dist/db/calendar.d.ts.map +1 -0
- package/dist/db/comments.d.ts.map +1 -1
- package/dist/db/handoffs.d.ts +25 -0
- package/dist/db/handoffs.d.ts.map +1 -1
- package/dist/db/machines.d.ts +19 -6
- package/dist/db/machines.d.ts.map +1 -1
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/project-knowledge.d.ts +88 -0
- package/dist/db/project-knowledge.d.ts.map +1 -0
- package/dist/db/project-risks.d.ts +139 -0
- package/dist/db/project-risks.d.ts.map +1 -0
- package/dist/db/retrospectives.d.ts +98 -0
- package/dist/db/retrospectives.d.ts.map +1 -0
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/task-crud.d.ts.map +1 -1
- package/dist/db/task-relations.d.ts +69 -9
- package/dist/db/task-relations.d.ts.map +1 -1
- package/dist/db/task-runs.d.ts +3 -0
- package/dist/db/task-runs.d.ts.map +1 -1
- package/dist/db/tasks.d.ts +6 -2
- package/dist/db/tasks.d.ts.map +1 -1
- package/dist/index.d.ts +74 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24662 -11829
- package/dist/json-contracts.d.ts.map +1 -1
- package/dist/lib/agent-replay-simulator.d.ts +66 -0
- package/dist/lib/agent-replay-simulator.d.ts.map +1 -0
- package/dist/lib/audit-ledger.d.ts +59 -0
- package/dist/lib/audit-ledger.d.ts.map +1 -0
- package/dist/lib/branch-work-plans.d.ts +46 -0
- package/dist/lib/branch-work-plans.d.ts.map +1 -0
- package/dist/lib/capacity-forecasts.d.ts +70 -0
- package/dist/lib/capacity-forecasts.d.ts.map +1 -0
- package/dist/lib/cli-help.d.ts +38 -0
- package/dist/lib/cli-help.d.ts.map +1 -0
- package/dist/lib/config.d.ts +217 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/context-packs.d.ts +26 -3
- package/dist/lib/context-packs.d.ts.map +1 -1
- package/dist/lib/event-hooks.d.ts +1 -1
- package/dist/lib/event-hooks.d.ts.map +1 -1
- package/dist/lib/external-issue-importers.d.ts +60 -0
- package/dist/lib/external-issue-importers.d.ts.map +1 -0
- package/dist/lib/extract.d.ts +57 -0
- package/dist/lib/extract.d.ts.map +1 -1
- package/dist/lib/local-backups.d.ts +129 -0
- package/dist/lib/local-backups.d.ts.map +1 -0
- package/dist/lib/local-bridge.d.ts +3 -1
- package/dist/lib/local-bridge.d.ts.map +1 -1
- package/dist/lib/local-extensions.d.ts +92 -0
- package/dist/lib/local-extensions.d.ts.map +1 -0
- package/dist/lib/local-notifications.d.ts +55 -0
- package/dist/lib/local-notifications.d.ts.map +1 -0
- package/dist/lib/local-reports.d.ts +149 -0
- package/dist/lib/local-reports.d.ts.map +1 -0
- package/dist/lib/local-snapshots.d.ts +66 -0
- package/dist/lib/local-snapshots.d.ts.map +1 -0
- package/dist/lib/mention-resolver.d.ts +43 -0
- package/dist/lib/mention-resolver.d.ts.map +1 -0
- package/dist/lib/natural-language-intake.d.ts +56 -0
- package/dist/lib/natural-language-intake.d.ts.map +1 -0
- package/dist/lib/onboarding-fixtures.d.ts +31 -0
- package/dist/lib/onboarding-fixtures.d.ts.map +1 -0
- package/dist/lib/public-release-gate.d.ts +7 -0
- package/dist/lib/public-release-gate.d.ts.map +1 -1
- package/dist/lib/redaction.d.ts +9 -0
- package/dist/lib/redaction.d.ts.map +1 -1
- package/dist/lib/release-compatibility.d.ts +59 -0
- package/dist/lib/release-compatibility.d.ts.map +1 -0
- package/dist/lib/release-notes.d.ts +81 -0
- package/dist/lib/release-notes.d.ts.map +1 -0
- package/dist/lib/retention-cleanup.d.ts +63 -0
- package/dist/lib/retention-cleanup.d.ts.map +1 -0
- package/dist/lib/review-queues.d.ts +98 -0
- package/dist/lib/review-queues.d.ts.map +1 -0
- package/dist/lib/roadmaps.d.ts +133 -0
- package/dist/lib/roadmaps.d.ts.map +1 -0
- package/dist/lib/scale-hardening.d.ts +74 -0
- package/dist/lib/scale-hardening.d.ts.map +1 -0
- package/dist/lib/sdk-integration-fixtures.d.ts +65 -0
- package/dist/lib/sdk-integration-fixtures.d.ts.map +1 -0
- package/dist/lib/terminal-notifications.d.ts +53 -0
- package/dist/lib/terminal-notifications.d.ts.map +1 -0
- package/dist/lib/todos-md.d.ts.map +1 -1
- package/dist/lib/tui-dashboard.d.ts +49 -0
- package/dist/lib/tui-dashboard.d.ts.map +1 -0
- package/dist/lib/usage-ledger.d.ts +82 -0
- package/dist/lib/usage-ledger.d.ts.map +1 -0
- package/dist/lib/workflow-prompts.d.ts +38 -0
- package/dist/lib/workflow-prompts.d.ts.map +1 -0
- package/dist/lib/workflow-states.d.ts +70 -0
- package/dist/lib/workflow-states.d.ts.map +1 -0
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +25922 -13067
- package/dist/mcp/token-utils.d.ts.map +1 -1
- package/dist/mcp/tools/code-tools.d.ts.map +1 -1
- package/dist/mcp/tools/machines.d.ts.map +1 -1
- package/dist/mcp/tools/task-adv-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-auto-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-crud.d.ts.map +1 -1
- package/dist/mcp/tools/task-meta-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-rel-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-resources.d.ts.map +1 -1
- package/dist/mcp/tools/workflow-prompts.d.ts +3 -0
- package/dist/mcp/tools/workflow-prompts.d.ts.map +1 -0
- package/dist/mcp.js +107 -2
- package/dist/registry.js +15919 -6050
- package/dist/server/index.js +645 -143
- package/dist/storage.js +2516 -159
- package/dist/types/index.d.ts +214 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- 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 {
|
|
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:
|
|
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 =
|
|
2367
|
+
let dir = resolve2(startDir);
|
|
1861
2368
|
while (true) {
|
|
1862
2369
|
const candidate = join(dir, ".todos", "todos.db");
|
|
1863
|
-
if (
|
|
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 =
|
|
2380
|
+
let dir = resolve2(startDir);
|
|
1874
2381
|
while (true) {
|
|
1875
|
-
if (
|
|
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 (!
|
|
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(
|
|
1916
|
-
if (!
|
|
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
|
|
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
|
|
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 (!
|
|
2527
|
+
if (!existsSync3(dir))
|
|
2021
2528
|
mkdirSync2(dir, { recursive: true });
|
|
2022
2529
|
}
|
|
2023
2530
|
function listJsonFiles(dir) {
|
|
2024
|
-
if (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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
|
|
3040
|
+
import { relative as relative2, resolve as resolve4 } from "path";
|
|
2459
3041
|
|
|
2460
3042
|
// src/lib/workspace-trust.ts
|
|
2461
|
-
import { relative, resolve as
|
|
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
|
|
3089
|
+
return resolve3(path);
|
|
2508
3090
|
}
|
|
2509
|
-
function
|
|
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:
|
|
2570
|
-
command_denylist:
|
|
2571
|
-
tool_permissions:
|
|
2572
|
-
write_scopes:
|
|
2573
|
-
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 =
|
|
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
|
|
3226
|
+
return resolve4(path);
|
|
2645
3227
|
}
|
|
2646
|
-
function
|
|
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 =
|
|
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:
|
|
2736
|
-
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:
|
|
2739
|
-
env_allowlist:
|
|
2740
|
-
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
5819
|
+
function getEscalatedTasks(opts = {}, db, at = new Date) {
|
|
5225
5820
|
const d = db || getDatabase();
|
|
5226
|
-
|
|
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
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
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
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
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
|
|
6929
|
+
function getTaskVerifications(taskId, db) {
|
|
5250
6930
|
const d = db || getDatabase();
|
|
5251
|
-
|
|
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
|
|
6933
|
+
function getTaskTraceability(taskId, db) {
|
|
5255
6934
|
const d = db || getDatabase();
|
|
5256
|
-
|
|
5257
|
-
|
|
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
|
|
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
|
|
5264
|
-
if (
|
|
5265
|
-
throw new
|
|
5266
|
-
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
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
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
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
|
-
|
|
5285
|
-
|
|
5286
|
-
|
|
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
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
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:
|
|
5293
|
-
payload: { 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
|
|
7201
|
+
function listTaskRuns(taskId, db) {
|
|
5298
7202
|
const d = db || getDatabase();
|
|
5299
|
-
const
|
|
5300
|
-
return
|
|
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
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
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
|
|
5437
|
-
const
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5441
|
-
|
|
5442
|
-
|
|
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 = {}) {
|