@hasna/todos 0.11.40 → 0.11.42
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 +527 -1
- package/dashboard/dist/assets/{index-B-w1tUlm.js → index-CVF1vn7Z.js} +23 -23
- package/dashboard/dist/assets/index-DJm6m6Yy.css +1 -0
- package/dashboard/dist/index.html +2 -2
- package/dist/cli/commands/agent-commands.d.ts.map +1 -1
- package/dist/cli/commands/config-serve-commands.d.ts.map +1 -1
- package/dist/cli/commands/mcp-hooks-commands.d.ts.map +1 -1
- package/dist/cli/commands/plan-template-commands.d.ts.map +1 -1
- 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/index.js +17662 -8245
- package/dist/cli-mcp-parity.d.ts +41 -0
- package/dist/cli-mcp-parity.d.ts.map +1 -0
- package/dist/contracts.d.ts +4 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +6396 -24
- package/dist/db/database.d.ts +2 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/handoffs.d.ts +27 -1
- package/dist/db/handoffs.d.ts.map +1 -1
- package/dist/db/inbox.d.ts +47 -0
- package/dist/db/inbox.d.ts.map +1 -0
- package/dist/db/migrations.d.ts.map +1 -1
- package/dist/db/plans.d.ts.map +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/task-commits.d.ts +51 -0
- package/dist/db/task-commits.d.ts.map +1 -1
- package/dist/db/task-crud.d.ts.map +1 -1
- package/dist/db/task-lifecycle.d.ts +16 -1
- package/dist/db/task-lifecycle.d.ts.map +1 -1
- package/dist/db/task-runs.d.ts +130 -0
- package/dist/db/task-runs.d.ts.map +1 -0
- package/dist/db/tasks.d.ts +2 -2
- package/dist/db/tasks.d.ts.map +1 -1
- package/dist/index.d.ts +49 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13353 -6350
- package/dist/json-contracts.d.ts.map +1 -1
- package/dist/lib/activity-timeline.d.ts +43 -0
- package/dist/lib/activity-timeline.d.ts.map +1 -0
- package/dist/lib/agent-run-dispatcher.d.ts +62 -0
- package/dist/lib/agent-run-dispatcher.d.ts.map +1 -0
- package/dist/lib/approval-gates.d.ts +52 -0
- package/dist/lib/approval-gates.d.ts.map +1 -0
- package/dist/lib/artifact-store.d.ts +68 -0
- package/dist/lib/artifact-store.d.ts.map +1 -0
- package/dist/lib/auto-assign.d.ts +3 -5
- package/dist/lib/auto-assign.d.ts.map +1 -1
- package/dist/lib/config.d.ts +121 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/context-packs.d.ts +140 -0
- package/dist/lib/context-packs.d.ts.map +1 -0
- package/dist/lib/doctor.d.ts +46 -0
- package/dist/lib/doctor.d.ts.map +1 -0
- package/dist/lib/event-hooks.d.ts +58 -0
- package/dist/lib/event-hooks.d.ts.map +1 -0
- package/dist/lib/local-bridge.d.ts +79 -0
- package/dist/lib/local-bridge.d.ts.map +1 -0
- package/dist/lib/local-encryption.d.ts +94 -0
- package/dist/lib/local-encryption.d.ts.map +1 -0
- package/dist/lib/local-fields.d.ts +33 -0
- package/dist/lib/local-fields.d.ts.map +1 -0
- package/dist/lib/policy-packs.d.ts +87 -0
- package/dist/lib/policy-packs.d.ts.map +1 -0
- package/dist/lib/project-bootstrap.d.ts +35 -0
- package/dist/lib/project-bootstrap.d.ts.map +1 -0
- package/dist/lib/public-release-gate.d.ts +50 -0
- package/dist/lib/public-release-gate.d.ts.map +1 -0
- package/dist/lib/redaction.d.ts +3 -0
- package/dist/lib/redaction.d.ts.map +1 -0
- package/dist/lib/runner-sandbox.d.ts +50 -0
- package/dist/lib/runner-sandbox.d.ts.map +1 -0
- package/dist/lib/saved-search-views.d.ts +60 -0
- package/dist/lib/saved-search-views.d.ts.map +1 -0
- package/dist/lib/task-contracts.d.ts +75 -0
- package/dist/lib/task-contracts.d.ts.map +1 -0
- package/dist/lib/task-dedupe.d.ts +45 -0
- package/dist/lib/task-dedupe.d.ts.map +1 -0
- package/dist/lib/todos-md.d.ts +21 -0
- package/dist/lib/todos-md.d.ts.map +1 -0
- package/dist/lib/verification-providers.d.ts +54 -0
- package/dist/lib/verification-providers.d.ts.map +1 -0
- package/dist/lib/workspace-trust.d.ts +38 -0
- package/dist/lib/workspace-trust.d.ts.map +1 -0
- package/dist/mcp/index.js +9717 -3197
- package/dist/mcp/token-utils.d.ts +2 -2
- package/dist/mcp/token-utils.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-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/task-workflow-tools.d.ts.map +1 -1
- package/dist/mcp.js +88 -2
- package/dist/registry.d.ts +2 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +7061 -60
- package/dist/release-provenance.json +7 -0
- package/dist/sdk/types.d.ts +26 -1
- package/dist/sdk/types.d.ts.map +1 -1
- package/dist/server/index.js +1330 -129
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/storage.js +1054 -15
- package/dist/test/no-network.d.ts +7 -0
- package/dist/test/no-network.d.ts.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +10 -4
- package/dashboard/dist/assets/index-BXQ39iMX.css +0 -1
package/dist/server/index.js
CHANGED
|
@@ -846,6 +846,154 @@ var init_migrations = __esm(() => {
|
|
|
846
846
|
CREATE INDEX IF NOT EXISTS idx_api_keys_prefix ON api_keys(prefix);
|
|
847
847
|
CREATE INDEX IF NOT EXISTS idx_api_keys_active ON api_keys(revoked_at, expires_at);
|
|
848
848
|
INSERT OR IGNORE INTO _migrations (id) VALUES (50);
|
|
849
|
+
`,
|
|
850
|
+
`
|
|
851
|
+
CREATE TABLE IF NOT EXISTS task_git_refs (
|
|
852
|
+
id TEXT PRIMARY KEY,
|
|
853
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
854
|
+
ref_type TEXT NOT NULL CHECK(ref_type IN ('branch', 'pull_request')),
|
|
855
|
+
name TEXT NOT NULL,
|
|
856
|
+
url TEXT,
|
|
857
|
+
provider TEXT,
|
|
858
|
+
metadata TEXT DEFAULT '{}',
|
|
859
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
860
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
861
|
+
UNIQUE(task_id, ref_type, name)
|
|
862
|
+
);
|
|
863
|
+
CREATE INDEX IF NOT EXISTS idx_task_git_refs_task ON task_git_refs(task_id);
|
|
864
|
+
CREATE INDEX IF NOT EXISTS idx_task_git_refs_lookup ON task_git_refs(ref_type, name);
|
|
865
|
+
CREATE INDEX IF NOT EXISTS idx_task_git_refs_url ON task_git_refs(url);
|
|
866
|
+
|
|
867
|
+
CREATE TABLE IF NOT EXISTS task_verifications (
|
|
868
|
+
id TEXT PRIMARY KEY,
|
|
869
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
870
|
+
command TEXT NOT NULL,
|
|
871
|
+
status TEXT NOT NULL DEFAULT 'unknown' CHECK(status IN ('passed', 'failed', 'unknown')),
|
|
872
|
+
output_summary TEXT,
|
|
873
|
+
artifact_path TEXT,
|
|
874
|
+
agent_id TEXT,
|
|
875
|
+
run_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
876
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
877
|
+
);
|
|
878
|
+
CREATE INDEX IF NOT EXISTS idx_task_verifications_task ON task_verifications(task_id);
|
|
879
|
+
CREATE INDEX IF NOT EXISTS idx_task_verifications_status ON task_verifications(status);
|
|
880
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (51);
|
|
881
|
+
`,
|
|
882
|
+
`
|
|
883
|
+
CREATE TABLE IF NOT EXISTS task_runs (
|
|
884
|
+
id TEXT PRIMARY KEY,
|
|
885
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
886
|
+
agent_id TEXT,
|
|
887
|
+
title TEXT,
|
|
888
|
+
status TEXT NOT NULL DEFAULT 'running' CHECK(status IN ('running', 'completed', 'failed', 'cancelled')),
|
|
889
|
+
summary TEXT,
|
|
890
|
+
metadata TEXT DEFAULT '{}',
|
|
891
|
+
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
892
|
+
completed_at TEXT,
|
|
893
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
894
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
895
|
+
);
|
|
896
|
+
CREATE INDEX IF NOT EXISTS idx_task_runs_task ON task_runs(task_id);
|
|
897
|
+
CREATE INDEX IF NOT EXISTS idx_task_runs_agent ON task_runs(agent_id);
|
|
898
|
+
CREATE INDEX IF NOT EXISTS idx_task_runs_status ON task_runs(status);
|
|
899
|
+
CREATE INDEX IF NOT EXISTS idx_task_runs_started ON task_runs(started_at);
|
|
900
|
+
|
|
901
|
+
CREATE TABLE IF NOT EXISTS task_run_events (
|
|
902
|
+
id TEXT PRIMARY KEY,
|
|
903
|
+
run_id TEXT NOT NULL REFERENCES task_runs(id) ON DELETE CASCADE,
|
|
904
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
905
|
+
event_type TEXT NOT NULL CHECK(event_type IN ('started', 'progress', 'claim', 'comment', 'command', 'file', 'artifact', 'completed', 'failed', 'cancelled')),
|
|
906
|
+
message TEXT,
|
|
907
|
+
data TEXT DEFAULT '{}',
|
|
908
|
+
agent_id TEXT,
|
|
909
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
910
|
+
);
|
|
911
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_events_run ON task_run_events(run_id);
|
|
912
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_events_task ON task_run_events(task_id);
|
|
913
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_events_type ON task_run_events(event_type);
|
|
914
|
+
|
|
915
|
+
CREATE TABLE IF NOT EXISTS task_run_commands (
|
|
916
|
+
id TEXT PRIMARY KEY,
|
|
917
|
+
run_id TEXT NOT NULL REFERENCES task_runs(id) ON DELETE CASCADE,
|
|
918
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
919
|
+
command TEXT NOT NULL,
|
|
920
|
+
status TEXT NOT NULL DEFAULT 'unknown' CHECK(status IN ('passed', 'failed', 'unknown')),
|
|
921
|
+
exit_code INTEGER,
|
|
922
|
+
output_summary TEXT,
|
|
923
|
+
artifact_path TEXT,
|
|
924
|
+
agent_id TEXT,
|
|
925
|
+
started_at TEXT,
|
|
926
|
+
completed_at TEXT,
|
|
927
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
928
|
+
);
|
|
929
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_commands_run ON task_run_commands(run_id);
|
|
930
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_commands_task ON task_run_commands(task_id);
|
|
931
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_commands_status ON task_run_commands(status);
|
|
932
|
+
|
|
933
|
+
CREATE TABLE IF NOT EXISTS task_run_artifacts (
|
|
934
|
+
id TEXT PRIMARY KEY,
|
|
935
|
+
run_id TEXT NOT NULL REFERENCES task_runs(id) ON DELETE CASCADE,
|
|
936
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
937
|
+
path TEXT NOT NULL,
|
|
938
|
+
artifact_type TEXT,
|
|
939
|
+
description TEXT,
|
|
940
|
+
size_bytes INTEGER,
|
|
941
|
+
sha256 TEXT,
|
|
942
|
+
metadata TEXT DEFAULT '{}',
|
|
943
|
+
agent_id TEXT,
|
|
944
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
945
|
+
);
|
|
946
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_run ON task_run_artifacts(run_id);
|
|
947
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_task ON task_run_artifacts(task_id);
|
|
948
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_path ON task_run_artifacts(path);
|
|
949
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (52);
|
|
950
|
+
`,
|
|
951
|
+
`
|
|
952
|
+
CREATE TABLE IF NOT EXISTS inbox_items (
|
|
953
|
+
id TEXT PRIMARY KEY,
|
|
954
|
+
task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
|
|
955
|
+
source_type TEXT NOT NULL CHECK(source_type IN ('pasted_error', 'ci_log', 'git_context', 'github_issue', 'file', 'other')),
|
|
956
|
+
source_name TEXT,
|
|
957
|
+
source_url TEXT,
|
|
958
|
+
title TEXT NOT NULL,
|
|
959
|
+
body TEXT,
|
|
960
|
+
fingerprint TEXT NOT NULL UNIQUE,
|
|
961
|
+
status TEXT NOT NULL DEFAULT 'triaged' CHECK(status IN ('new', 'triaged', 'ignored')),
|
|
962
|
+
metadata TEXT DEFAULT '{}',
|
|
963
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
964
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
965
|
+
);
|
|
966
|
+
CREATE INDEX IF NOT EXISTS idx_inbox_items_task ON inbox_items(task_id);
|
|
967
|
+
CREATE INDEX IF NOT EXISTS idx_inbox_items_source ON inbox_items(source_type, source_name);
|
|
968
|
+
CREATE INDEX IF NOT EXISTS idx_inbox_items_status ON inbox_items(status);
|
|
969
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (53);
|
|
970
|
+
`,
|
|
971
|
+
`
|
|
972
|
+
ALTER TABLE handoffs ADD COLUMN session_id TEXT;
|
|
973
|
+
ALTER TABLE handoffs ADD COLUMN task_ids TEXT;
|
|
974
|
+
ALTER TABLE handoffs ADD COLUMN relevant_files TEXT;
|
|
975
|
+
ALTER TABLE handoffs ADD COLUMN run_ids TEXT;
|
|
976
|
+
CREATE TABLE IF NOT EXISTS handoff_acknowledgements (
|
|
977
|
+
handoff_id TEXT NOT NULL REFERENCES handoffs(id) ON DELETE CASCADE,
|
|
978
|
+
agent_id TEXT NOT NULL,
|
|
979
|
+
acknowledged_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
980
|
+
PRIMARY KEY (handoff_id, agent_id)
|
|
981
|
+
);
|
|
982
|
+
CREATE INDEX IF NOT EXISTS idx_handoff_acks_agent ON handoff_acknowledgements(agent_id, acknowledged_at);
|
|
983
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (54);
|
|
984
|
+
`,
|
|
985
|
+
`
|
|
986
|
+
CREATE TABLE IF NOT EXISTS saved_search_views (
|
|
987
|
+
id TEXT PRIMARY KEY,
|
|
988
|
+
name TEXT NOT NULL UNIQUE,
|
|
989
|
+
description TEXT,
|
|
990
|
+
scope TEXT NOT NULL DEFAULT 'tasks' CHECK(scope IN ('all', 'tasks', 'projects', 'plans', 'runs', 'comments')),
|
|
991
|
+
filters TEXT NOT NULL DEFAULT '{}',
|
|
992
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
993
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
994
|
+
);
|
|
995
|
+
CREATE INDEX IF NOT EXISTS idx_saved_search_views_scope ON saved_search_views(scope);
|
|
996
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (55);
|
|
849
997
|
`
|
|
850
998
|
];
|
|
851
999
|
});
|
|
@@ -930,6 +1078,17 @@ function ensureSchema(db) {
|
|
|
930
1078
|
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
931
1079
|
tag TEXT NOT NULL, PRIMARY KEY (task_id, tag)
|
|
932
1080
|
)`);
|
|
1081
|
+
ensureTable("task_dependencies", `
|
|
1082
|
+
CREATE TABLE task_dependencies (
|
|
1083
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1084
|
+
depends_on TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1085
|
+
external_project_id TEXT,
|
|
1086
|
+
external_task_id TEXT,
|
|
1087
|
+
PRIMARY KEY (task_id, depends_on),
|
|
1088
|
+
CHECK (task_id != depends_on)
|
|
1089
|
+
)`);
|
|
1090
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_dependencies_task ON task_dependencies(task_id)");
|
|
1091
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_dependencies_depends_on ON task_dependencies(depends_on)");
|
|
933
1092
|
ensureTable("task_history", `
|
|
934
1093
|
CREATE TABLE task_history (
|
|
935
1094
|
id TEXT PRIMARY KEY, task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
@@ -987,6 +1146,41 @@ function ensureSchema(db) {
|
|
|
987
1146
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
988
1147
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
989
1148
|
)`);
|
|
1149
|
+
ensureTable("handoffs", `
|
|
1150
|
+
CREATE TABLE handoffs (
|
|
1151
|
+
id TEXT PRIMARY KEY,
|
|
1152
|
+
agent_id TEXT,
|
|
1153
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
1154
|
+
session_id TEXT,
|
|
1155
|
+
summary TEXT NOT NULL,
|
|
1156
|
+
completed TEXT,
|
|
1157
|
+
in_progress TEXT,
|
|
1158
|
+
blockers TEXT,
|
|
1159
|
+
next_steps TEXT,
|
|
1160
|
+
task_ids TEXT,
|
|
1161
|
+
relevant_files TEXT,
|
|
1162
|
+
run_ids TEXT,
|
|
1163
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1164
|
+
)`);
|
|
1165
|
+
ensureTable("handoff_acknowledgements", `
|
|
1166
|
+
CREATE TABLE handoff_acknowledgements (
|
|
1167
|
+
handoff_id TEXT NOT NULL REFERENCES handoffs(id) ON DELETE CASCADE,
|
|
1168
|
+
agent_id TEXT NOT NULL,
|
|
1169
|
+
acknowledged_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1170
|
+
PRIMARY KEY (handoff_id, agent_id)
|
|
1171
|
+
)`);
|
|
1172
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_handoff_acks_agent ON handoff_acknowledgements(agent_id, acknowledged_at)");
|
|
1173
|
+
ensureTable("saved_search_views", `
|
|
1174
|
+
CREATE TABLE saved_search_views (
|
|
1175
|
+
id TEXT PRIMARY KEY,
|
|
1176
|
+
name TEXT NOT NULL UNIQUE,
|
|
1177
|
+
description TEXT,
|
|
1178
|
+
scope TEXT NOT NULL DEFAULT 'tasks' CHECK(scope IN ('all', 'tasks', 'projects', 'plans', 'runs', 'comments')),
|
|
1179
|
+
filters TEXT NOT NULL DEFAULT '{}',
|
|
1180
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1181
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1182
|
+
)`);
|
|
1183
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_saved_search_views_scope ON saved_search_views(scope)");
|
|
990
1184
|
ensureTable("task_relationships", `
|
|
991
1185
|
CREATE TABLE task_relationships (
|
|
992
1186
|
id TEXT PRIMARY KEY,
|
|
@@ -998,6 +1192,121 @@ function ensureSchema(db) {
|
|
|
998
1192
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
999
1193
|
CHECK (source_task_id != target_task_id)
|
|
1000
1194
|
)`);
|
|
1195
|
+
ensureTable("task_git_refs", `
|
|
1196
|
+
CREATE TABLE task_git_refs (
|
|
1197
|
+
id TEXT PRIMARY KEY,
|
|
1198
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1199
|
+
ref_type TEXT NOT NULL CHECK(ref_type IN ('branch', 'pull_request')),
|
|
1200
|
+
name TEXT NOT NULL,
|
|
1201
|
+
url TEXT,
|
|
1202
|
+
provider TEXT,
|
|
1203
|
+
metadata TEXT DEFAULT '{}',
|
|
1204
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1205
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1206
|
+
UNIQUE(task_id, ref_type, name)
|
|
1207
|
+
)`);
|
|
1208
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_git_refs_task ON task_git_refs(task_id)");
|
|
1209
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_git_refs_lookup ON task_git_refs(ref_type, name)");
|
|
1210
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_git_refs_url ON task_git_refs(url)");
|
|
1211
|
+
ensureTable("task_verifications", `
|
|
1212
|
+
CREATE TABLE task_verifications (
|
|
1213
|
+
id TEXT PRIMARY KEY,
|
|
1214
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1215
|
+
command TEXT NOT NULL,
|
|
1216
|
+
status TEXT NOT NULL DEFAULT 'unknown' CHECK(status IN ('passed', 'failed', 'unknown')),
|
|
1217
|
+
output_summary TEXT,
|
|
1218
|
+
artifact_path TEXT,
|
|
1219
|
+
agent_id TEXT,
|
|
1220
|
+
run_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1221
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1222
|
+
)`);
|
|
1223
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_verifications_task ON task_verifications(task_id)");
|
|
1224
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_verifications_status ON task_verifications(status)");
|
|
1225
|
+
ensureTable("task_runs", `
|
|
1226
|
+
CREATE TABLE task_runs (
|
|
1227
|
+
id TEXT PRIMARY KEY,
|
|
1228
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1229
|
+
agent_id TEXT,
|
|
1230
|
+
title TEXT,
|
|
1231
|
+
status TEXT NOT NULL DEFAULT 'running' CHECK(status IN ('running', 'completed', 'failed', 'cancelled')),
|
|
1232
|
+
summary TEXT,
|
|
1233
|
+
metadata TEXT DEFAULT '{}',
|
|
1234
|
+
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1235
|
+
completed_at TEXT,
|
|
1236
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1237
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1238
|
+
)`);
|
|
1239
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_runs_task ON task_runs(task_id)");
|
|
1240
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_runs_agent ON task_runs(agent_id)");
|
|
1241
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_runs_status ON task_runs(status)");
|
|
1242
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_runs_started ON task_runs(started_at)");
|
|
1243
|
+
ensureTable("task_run_events", `
|
|
1244
|
+
CREATE TABLE task_run_events (
|
|
1245
|
+
id TEXT PRIMARY KEY,
|
|
1246
|
+
run_id TEXT NOT NULL REFERENCES task_runs(id) ON DELETE CASCADE,
|
|
1247
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1248
|
+
event_type TEXT NOT NULL CHECK(event_type IN ('started', 'progress', 'claim', 'comment', 'command', 'file', 'artifact', 'completed', 'failed', 'cancelled')),
|
|
1249
|
+
message TEXT,
|
|
1250
|
+
data TEXT DEFAULT '{}',
|
|
1251
|
+
agent_id TEXT,
|
|
1252
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1253
|
+
)`);
|
|
1254
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_events_run ON task_run_events(run_id)");
|
|
1255
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_events_task ON task_run_events(task_id)");
|
|
1256
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_events_type ON task_run_events(event_type)");
|
|
1257
|
+
ensureTable("task_run_commands", `
|
|
1258
|
+
CREATE TABLE task_run_commands (
|
|
1259
|
+
id TEXT PRIMARY KEY,
|
|
1260
|
+
run_id TEXT NOT NULL REFERENCES task_runs(id) ON DELETE CASCADE,
|
|
1261
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1262
|
+
command TEXT NOT NULL,
|
|
1263
|
+
status TEXT NOT NULL DEFAULT 'unknown' CHECK(status IN ('passed', 'failed', 'unknown')),
|
|
1264
|
+
exit_code INTEGER,
|
|
1265
|
+
output_summary TEXT,
|
|
1266
|
+
artifact_path TEXT,
|
|
1267
|
+
agent_id TEXT,
|
|
1268
|
+
started_at TEXT,
|
|
1269
|
+
completed_at TEXT,
|
|
1270
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1271
|
+
)`);
|
|
1272
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_commands_run ON task_run_commands(run_id)");
|
|
1273
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_commands_task ON task_run_commands(task_id)");
|
|
1274
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_commands_status ON task_run_commands(status)");
|
|
1275
|
+
ensureTable("task_run_artifacts", `
|
|
1276
|
+
CREATE TABLE task_run_artifacts (
|
|
1277
|
+
id TEXT PRIMARY KEY,
|
|
1278
|
+
run_id TEXT NOT NULL REFERENCES task_runs(id) ON DELETE CASCADE,
|
|
1279
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1280
|
+
path TEXT NOT NULL,
|
|
1281
|
+
artifact_type TEXT,
|
|
1282
|
+
description TEXT,
|
|
1283
|
+
size_bytes INTEGER,
|
|
1284
|
+
sha256 TEXT,
|
|
1285
|
+
metadata TEXT DEFAULT '{}',
|
|
1286
|
+
agent_id TEXT,
|
|
1287
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1288
|
+
)`);
|
|
1289
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_run ON task_run_artifacts(run_id)");
|
|
1290
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_task ON task_run_artifacts(task_id)");
|
|
1291
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_path ON task_run_artifacts(path)");
|
|
1292
|
+
ensureTable("inbox_items", `
|
|
1293
|
+
CREATE TABLE inbox_items (
|
|
1294
|
+
id TEXT PRIMARY KEY,
|
|
1295
|
+
task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
|
|
1296
|
+
source_type TEXT NOT NULL CHECK(source_type IN ('pasted_error', 'ci_log', 'git_context', 'github_issue', 'file', 'other')),
|
|
1297
|
+
source_name TEXT,
|
|
1298
|
+
source_url TEXT,
|
|
1299
|
+
title TEXT NOT NULL,
|
|
1300
|
+
body TEXT,
|
|
1301
|
+
fingerprint TEXT NOT NULL UNIQUE,
|
|
1302
|
+
status TEXT NOT NULL DEFAULT 'triaged' CHECK(status IN ('new', 'triaged', 'ignored')),
|
|
1303
|
+
metadata TEXT DEFAULT '{}',
|
|
1304
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1305
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1306
|
+
)`);
|
|
1307
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_inbox_items_task ON inbox_items(task_id)");
|
|
1308
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_inbox_items_source ON inbox_items(source_type, source_name)");
|
|
1309
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_inbox_items_status ON inbox_items(status)");
|
|
1001
1310
|
ensureTable("kg_edges", `
|
|
1002
1311
|
CREATE TABLE kg_edges (
|
|
1003
1312
|
id TEXT PRIMARY KEY,
|
|
@@ -1159,6 +1468,10 @@ function ensureSchema(db) {
|
|
|
1159
1468
|
ensureColumn("orgs", "synced_at", "TEXT");
|
|
1160
1469
|
ensureColumn("handoffs", "machine_id", "TEXT");
|
|
1161
1470
|
ensureColumn("handoffs", "synced_at", "TEXT");
|
|
1471
|
+
ensureColumn("handoffs", "session_id", "TEXT");
|
|
1472
|
+
ensureColumn("handoffs", "task_ids", "TEXT");
|
|
1473
|
+
ensureColumn("handoffs", "relevant_files", "TEXT");
|
|
1474
|
+
ensureColumn("handoffs", "run_ids", "TEXT");
|
|
1162
1475
|
ensureColumn("task_checklists", "machine_id", "TEXT");
|
|
1163
1476
|
ensureColumn("project_sources", "machine_id", "TEXT");
|
|
1164
1477
|
ensureColumn("project_sources", "synced_at", "TEXT");
|
|
@@ -1363,6 +1676,7 @@ __export(exports_database, {
|
|
|
1363
1676
|
now: () => now,
|
|
1364
1677
|
lockExpiryCutoff: () => lockExpiryCutoff,
|
|
1365
1678
|
isLockExpired: () => isLockExpired,
|
|
1679
|
+
getDatabasePath: () => getDatabasePath,
|
|
1366
1680
|
getDatabase: () => getDatabase,
|
|
1367
1681
|
closeDatabase: () => closeDatabase,
|
|
1368
1682
|
clearExpiredLocks: () => clearExpiredLocks,
|
|
@@ -1424,6 +1738,9 @@ function getDbPath() {
|
|
|
1424
1738
|
}
|
|
1425
1739
|
return newPath;
|
|
1426
1740
|
}
|
|
1741
|
+
function getDatabasePath() {
|
|
1742
|
+
return getDbPath();
|
|
1743
|
+
}
|
|
1427
1744
|
function ensureDir(filePath) {
|
|
1428
1745
|
if (isInMemoryDb(filePath))
|
|
1429
1746
|
return;
|
|
@@ -1461,12 +1778,12 @@ function now() {
|
|
|
1461
1778
|
function uuid() {
|
|
1462
1779
|
return crypto.randomUUID();
|
|
1463
1780
|
}
|
|
1464
|
-
function isLockExpired(lockedAt) {
|
|
1781
|
+
function isLockExpired(lockedAt, nowMs = Date.now()) {
|
|
1465
1782
|
if (!lockedAt)
|
|
1466
1783
|
return true;
|
|
1467
1784
|
const lockTime = new Date(lockedAt).getTime();
|
|
1468
1785
|
const expiryMs = LOCK_EXPIRY_MINUTES * 60 * 1000;
|
|
1469
|
-
return
|
|
1786
|
+
return nowMs - lockTime > expiryMs;
|
|
1470
1787
|
}
|
|
1471
1788
|
function lockExpiryCutoff(nowMs = Date.now()) {
|
|
1472
1789
|
const expiryMs = LOCK_EXPIRY_MINUTES * 60 * 1000;
|
|
@@ -1516,6 +1833,498 @@ var init_database = __esm(() => {
|
|
|
1516
1833
|
ALLOWED_TABLES = new Set(["tasks", "projects", "agents", "plans", "task_lists", "task_templates"]);
|
|
1517
1834
|
});
|
|
1518
1835
|
|
|
1836
|
+
// src/lib/recurrence.ts
|
|
1837
|
+
function parseRecurrenceRule(rule) {
|
|
1838
|
+
const normalized = rule.trim().toLowerCase();
|
|
1839
|
+
if (normalized === "every weekday" || normalized === "every weekdays") {
|
|
1840
|
+
return { type: "specific_days", days: [1, 2, 3, 4, 5] };
|
|
1841
|
+
}
|
|
1842
|
+
if (normalized === "every day" || normalized === "daily") {
|
|
1843
|
+
return { type: "interval", interval: 1, unit: "day" };
|
|
1844
|
+
}
|
|
1845
|
+
if (normalized === "every week" || normalized === "weekly") {
|
|
1846
|
+
return { type: "interval", interval: 1, unit: "week" };
|
|
1847
|
+
}
|
|
1848
|
+
if (normalized === "every month" || normalized === "monthly") {
|
|
1849
|
+
return { type: "interval", interval: 1, unit: "month" };
|
|
1850
|
+
}
|
|
1851
|
+
const intervalMatch = normalized.match(/^every\s+(\d+)\s+(day|week|month)s?$/);
|
|
1852
|
+
if (intervalMatch) {
|
|
1853
|
+
return {
|
|
1854
|
+
type: "interval",
|
|
1855
|
+
interval: parseInt(intervalMatch[1], 10),
|
|
1856
|
+
unit: intervalMatch[2]
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
const daysMatch = normalized.match(/^every\s+(.+)$/);
|
|
1860
|
+
if (daysMatch) {
|
|
1861
|
+
const dayParts = daysMatch[1].split(/[,\s]+/).map((d) => d.trim()).filter(Boolean);
|
|
1862
|
+
const days = [];
|
|
1863
|
+
for (const part of dayParts) {
|
|
1864
|
+
const dayNum = DAY_NAMES[part];
|
|
1865
|
+
if (dayNum !== undefined) {
|
|
1866
|
+
days.push(dayNum);
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
if (days.length > 0) {
|
|
1870
|
+
return { type: "specific_days", days: days.sort((a, b) => a - b) };
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
throw new Error(`Invalid recurrence rule: "${rule}". Supported formats: "every day", "every weekday", "every week", "every 2 weeks", "every month", "every N days/weeks/months", "every monday", "every mon,wed,fri"`);
|
|
1874
|
+
}
|
|
1875
|
+
function isValidRecurrenceRule(rule) {
|
|
1876
|
+
try {
|
|
1877
|
+
parseRecurrenceRule(rule);
|
|
1878
|
+
return true;
|
|
1879
|
+
} catch {
|
|
1880
|
+
return false;
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
function nextOccurrence(rule, from) {
|
|
1884
|
+
const parsed = parseRecurrenceRule(rule);
|
|
1885
|
+
const base = from || new Date;
|
|
1886
|
+
if (parsed.type === "interval") {
|
|
1887
|
+
const next = new Date(base);
|
|
1888
|
+
if (parsed.unit === "day") {
|
|
1889
|
+
next.setDate(next.getDate() + parsed.interval);
|
|
1890
|
+
} else if (parsed.unit === "week") {
|
|
1891
|
+
next.setDate(next.getDate() + parsed.interval * 7);
|
|
1892
|
+
} else if (parsed.unit === "month") {
|
|
1893
|
+
next.setMonth(next.getMonth() + parsed.interval);
|
|
1894
|
+
}
|
|
1895
|
+
return next.toISOString();
|
|
1896
|
+
}
|
|
1897
|
+
if (parsed.type === "specific_days") {
|
|
1898
|
+
const currentDay = base.getDay();
|
|
1899
|
+
const days = parsed.days;
|
|
1900
|
+
let daysToAdd = Infinity;
|
|
1901
|
+
for (const day of days) {
|
|
1902
|
+
let diff = day - currentDay;
|
|
1903
|
+
if (diff <= 0)
|
|
1904
|
+
diff += 7;
|
|
1905
|
+
if (diff < daysToAdd)
|
|
1906
|
+
daysToAdd = diff;
|
|
1907
|
+
}
|
|
1908
|
+
const next = new Date(base);
|
|
1909
|
+
next.setDate(next.getDate() + daysToAdd);
|
|
1910
|
+
return next.toISOString();
|
|
1911
|
+
}
|
|
1912
|
+
throw new Error(`Cannot calculate next occurrence for rule: "${rule}"`);
|
|
1913
|
+
}
|
|
1914
|
+
var DAY_NAMES;
|
|
1915
|
+
var init_recurrence = __esm(() => {
|
|
1916
|
+
DAY_NAMES = {
|
|
1917
|
+
sunday: 0,
|
|
1918
|
+
sun: 0,
|
|
1919
|
+
monday: 1,
|
|
1920
|
+
mon: 1,
|
|
1921
|
+
tuesday: 2,
|
|
1922
|
+
tue: 2,
|
|
1923
|
+
wednesday: 3,
|
|
1924
|
+
wed: 3,
|
|
1925
|
+
thursday: 4,
|
|
1926
|
+
thu: 4,
|
|
1927
|
+
friday: 5,
|
|
1928
|
+
fri: 5,
|
|
1929
|
+
saturday: 6,
|
|
1930
|
+
sat: 6
|
|
1931
|
+
};
|
|
1932
|
+
});
|
|
1933
|
+
|
|
1934
|
+
// src/lib/doctor.ts
|
|
1935
|
+
var exports_doctor = {};
|
|
1936
|
+
__export(exports_doctor, {
|
|
1937
|
+
runTodosDoctor: () => runTodosDoctor
|
|
1938
|
+
});
|
|
1939
|
+
import { chmodSync, copyFileSync, existsSync as existsSync5, mkdirSync as mkdirSync4, statSync as statSync2 } from "fs";
|
|
1940
|
+
import { basename, dirname as dirname5, join as join4 } from "path";
|
|
1941
|
+
function tableExists(db, table) {
|
|
1942
|
+
return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
|
|
1943
|
+
}
|
|
1944
|
+
function quoteIdent(identifier) {
|
|
1945
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
1946
|
+
}
|
|
1947
|
+
function getTableColumns(db, table) {
|
|
1948
|
+
try {
|
|
1949
|
+
const rows = db.query(`PRAGMA table_info(${quoteIdent(table)})`).all();
|
|
1950
|
+
return new Set(rows.map((row) => row.name));
|
|
1951
|
+
} catch {
|
|
1952
|
+
return new Set;
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
function countQuery(db, sql) {
|
|
1956
|
+
try {
|
|
1957
|
+
const row = db.query(sql).get();
|
|
1958
|
+
return row?.count ?? 0;
|
|
1959
|
+
} catch {
|
|
1960
|
+
return 0;
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
function getMigrationLevel(db) {
|
|
1964
|
+
try {
|
|
1965
|
+
const row = db.query("SELECT MAX(id) as max_id FROM _migrations").get();
|
|
1966
|
+
return row?.max_id ?? 0;
|
|
1967
|
+
} catch {
|
|
1968
|
+
return 0;
|
|
1969
|
+
}
|
|
1970
|
+
}
|
|
1971
|
+
function addCheck(checks, check) {
|
|
1972
|
+
checks.push(check);
|
|
1973
|
+
}
|
|
1974
|
+
function listUserTables(db) {
|
|
1975
|
+
return db.query("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").all().map((row) => row.name).sort();
|
|
1976
|
+
}
|
|
1977
|
+
function findCorruptJsonMetadata(db) {
|
|
1978
|
+
const corrupt = [];
|
|
1979
|
+
for (const table of listUserTables(db)) {
|
|
1980
|
+
const columns = getTableColumns(db, table);
|
|
1981
|
+
if (!columns.has("metadata"))
|
|
1982
|
+
continue;
|
|
1983
|
+
const rows = db.query(`SELECT rowid, metadata FROM ${quoteIdent(table)} WHERE metadata IS NOT NULL AND metadata != ''`).all();
|
|
1984
|
+
for (const row of rows) {
|
|
1985
|
+
try {
|
|
1986
|
+
JSON.parse(row.metadata);
|
|
1987
|
+
} catch {
|
|
1988
|
+
corrupt.push({ table, column: "metadata", rowid: row.rowid });
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
return corrupt;
|
|
1993
|
+
}
|
|
1994
|
+
function getIndexColumns(db, indexName) {
|
|
1995
|
+
return db.query(`PRAGMA index_info(${quoteIdent(indexName)})`).all().map((row) => row.name).filter(Boolean);
|
|
1996
|
+
}
|
|
1997
|
+
function findDuplicateIndexes(db) {
|
|
1998
|
+
const duplicates = [];
|
|
1999
|
+
for (const table of listUserTables(db)) {
|
|
2000
|
+
const indexes = db.query(`PRAGMA index_list(${quoteIdent(table)})`).all();
|
|
2001
|
+
const groups = new Map;
|
|
2002
|
+
for (const index of indexes) {
|
|
2003
|
+
if (index.origin === "pk" || index.name.startsWith("sqlite_autoindex"))
|
|
2004
|
+
continue;
|
|
2005
|
+
const columns = getIndexColumns(db, index.name);
|
|
2006
|
+
if (columns.length === 0)
|
|
2007
|
+
continue;
|
|
2008
|
+
const key = `${index.unique}:${columns.join(",")}`;
|
|
2009
|
+
const current = groups.get(key) ?? [];
|
|
2010
|
+
current.push({ name: index.name, origin: index.origin, columns });
|
|
2011
|
+
groups.set(key, current);
|
|
2012
|
+
}
|
|
2013
|
+
for (const group of groups.values()) {
|
|
2014
|
+
if (group.length < 2)
|
|
2015
|
+
continue;
|
|
2016
|
+
const [kept, ...rest] = group;
|
|
2017
|
+
for (const duplicate of rest) {
|
|
2018
|
+
duplicates.push({ table, duplicate: duplicate.name, kept: kept.name, columns: duplicate.columns });
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
return duplicates;
|
|
2023
|
+
}
|
|
2024
|
+
function findMissingProjectRoots(db) {
|
|
2025
|
+
if (!tableExists(db, "projects"))
|
|
2026
|
+
return 0;
|
|
2027
|
+
let missing = 0;
|
|
2028
|
+
const rows = db.query("SELECT path FROM projects WHERE path IS NOT NULL AND path != ''").all();
|
|
2029
|
+
for (const row of rows) {
|
|
2030
|
+
if (/^[a-z]+:\/\//i.test(row.path))
|
|
2031
|
+
continue;
|
|
2032
|
+
if (!row.path.startsWith("/"))
|
|
2033
|
+
continue;
|
|
2034
|
+
if (!existsSync5(row.path))
|
|
2035
|
+
missing++;
|
|
2036
|
+
}
|
|
2037
|
+
return missing;
|
|
2038
|
+
}
|
|
2039
|
+
function addTaskStateChecks(db, checks) {
|
|
2040
|
+
if (!tableExists(db, "tasks"))
|
|
2041
|
+
return;
|
|
2042
|
+
const columns = getTableColumns(db, "tasks");
|
|
2043
|
+
const staleCutoff = new Date(Date.now() - 30 * 60 * 1000).toISOString();
|
|
2044
|
+
if (columns.has("updated_at") && columns.has("status")) {
|
|
2045
|
+
const staleTasks = countQuery(db, `SELECT COUNT(*) as count FROM tasks WHERE status = 'in_progress' AND updated_at < '${staleCutoff}'`);
|
|
2046
|
+
if (staleTasks > 0) {
|
|
2047
|
+
addCheck(checks, {
|
|
2048
|
+
severity: "warn",
|
|
2049
|
+
type: "stale_tasks",
|
|
2050
|
+
message: `${staleTasks} tasks stuck in_progress for more than 30 minutes`,
|
|
2051
|
+
count: staleTasks,
|
|
2052
|
+
repairable: false
|
|
2053
|
+
});
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
if (columns.has("recurrence_rule") && columns.has("status")) {
|
|
2057
|
+
const dueAtSelect = columns.has("due_at") ? "due_at" : "NULL as due_at";
|
|
2058
|
+
const recurring = db.query(`SELECT recurrence_rule, ${dueAtSelect} FROM tasks WHERE status IN ('pending', 'in_progress') AND recurrence_rule IS NOT NULL AND recurrence_rule != ''`).all();
|
|
2059
|
+
const invalidRecurrence = recurring.filter((task) => !isValidRecurrenceRule(task.recurrence_rule));
|
|
2060
|
+
if (invalidRecurrence.length > 0) {
|
|
2061
|
+
addCheck(checks, {
|
|
2062
|
+
severity: "error",
|
|
2063
|
+
type: "invalid_recurrence",
|
|
2064
|
+
message: `${invalidRecurrence.length} tasks have invalid recurrence rules`,
|
|
2065
|
+
count: invalidRecurrence.length,
|
|
2066
|
+
repairable: false
|
|
2067
|
+
});
|
|
2068
|
+
}
|
|
2069
|
+
const nowIso = new Date().toISOString();
|
|
2070
|
+
const overdueRecurring = recurring.filter((task) => task.due_at !== null && task.due_at < nowIso);
|
|
2071
|
+
if (overdueRecurring.length > 0) {
|
|
2072
|
+
addCheck(checks, {
|
|
2073
|
+
severity: "warn",
|
|
2074
|
+
type: "overdue_recurring",
|
|
2075
|
+
message: `${overdueRecurring.length} recurring tasks are past due`,
|
|
2076
|
+
count: overdueRecurring.length,
|
|
2077
|
+
repairable: false
|
|
2078
|
+
});
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
function databasePermissionsAreUnsafe(dbPath) {
|
|
2083
|
+
if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
|
|
2084
|
+
return false;
|
|
2085
|
+
try {
|
|
2086
|
+
return (statSync2(dbPath).mode & 63) !== 0;
|
|
2087
|
+
} catch {
|
|
2088
|
+
return false;
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
function createBackup(dbPath) {
|
|
2092
|
+
if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
|
|
2093
|
+
return;
|
|
2094
|
+
if (!existsSync5(dbPath))
|
|
2095
|
+
return;
|
|
2096
|
+
const stamp = now().replace(/[:.]/g, "-");
|
|
2097
|
+
const backupDir = join4(dirname5(dbPath), `${basename(dbPath)}.backup-${stamp}`);
|
|
2098
|
+
const files = [];
|
|
2099
|
+
mkdirSync4(backupDir, { recursive: true });
|
|
2100
|
+
for (const source of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
|
|
2101
|
+
if (!existsSync5(source))
|
|
2102
|
+
continue;
|
|
2103
|
+
const target = join4(backupDir, basename(source));
|
|
2104
|
+
copyFileSync(source, target);
|
|
2105
|
+
files.push(target);
|
|
2106
|
+
}
|
|
2107
|
+
return files.length > 0 ? { path: backupDir, files } : undefined;
|
|
2108
|
+
}
|
|
2109
|
+
function pushRepair(repairs, type, message, applied, count) {
|
|
2110
|
+
repairs.push({ type, message, applied, count });
|
|
2111
|
+
}
|
|
2112
|
+
function summarize2(checks, repairs) {
|
|
2113
|
+
return {
|
|
2114
|
+
errors: checks.filter((check) => check.severity === "error").length,
|
|
2115
|
+
warnings: checks.filter((check) => check.severity === "warn").length,
|
|
2116
|
+
infos: checks.filter((check) => check.severity === "info").length,
|
|
2117
|
+
repairable: checks.filter((check) => check.repairable).length,
|
|
2118
|
+
applied: repairs.filter((repair) => repair.applied).length
|
|
2119
|
+
};
|
|
2120
|
+
}
|
|
2121
|
+
function deleteOrphans(db, table, where) {
|
|
2122
|
+
const before = countQuery(db, `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`);
|
|
2123
|
+
if (before > 0)
|
|
2124
|
+
db.run(`DELETE FROM ${table} WHERE ${where}`);
|
|
2125
|
+
return before;
|
|
2126
|
+
}
|
|
2127
|
+
function runTodosDoctor(options = {}) {
|
|
2128
|
+
const db = options.db ?? getDatabase();
|
|
2129
|
+
const dbPath = options.dbPath ?? getDatabasePath();
|
|
2130
|
+
const apply = options.apply === true;
|
|
2131
|
+
const checks = [];
|
|
2132
|
+
const repairs = [];
|
|
2133
|
+
const migrationCurrent = getMigrationLevel(db);
|
|
2134
|
+
const migrationExpected = MIGRATIONS.length;
|
|
2135
|
+
if (migrationCurrent < migrationExpected) {
|
|
2136
|
+
addCheck(checks, {
|
|
2137
|
+
severity: "error",
|
|
2138
|
+
type: "migration_level",
|
|
2139
|
+
message: `Migration level ${migrationCurrent}; expected ${migrationExpected}`,
|
|
2140
|
+
repairable: true
|
|
2141
|
+
});
|
|
2142
|
+
} else {
|
|
2143
|
+
addCheck(checks, {
|
|
2144
|
+
severity: "info",
|
|
2145
|
+
type: "migration_level",
|
|
2146
|
+
message: `Schema at migration ${migrationCurrent}`
|
|
2147
|
+
});
|
|
2148
|
+
}
|
|
2149
|
+
const missingTables = REQUIRED_TABLES.filter((table) => !tableExists(db, table));
|
|
2150
|
+
if (missingTables.length > 0) {
|
|
2151
|
+
addCheck(checks, {
|
|
2152
|
+
severity: "error",
|
|
2153
|
+
type: "missing_schema_tables",
|
|
2154
|
+
message: `Missing schema tables: ${missingTables.join(", ")}`,
|
|
2155
|
+
count: missingTables.length,
|
|
2156
|
+
repairable: true
|
|
2157
|
+
});
|
|
2158
|
+
}
|
|
2159
|
+
const orphanedParents = tableExists(db, "tasks") ? countQuery(db, "SELECT COUNT(*) as count FROM tasks t WHERE t.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM tasks p WHERE p.id = t.parent_id)") : 0;
|
|
2160
|
+
if (orphanedParents > 0) {
|
|
2161
|
+
addCheck(checks, {
|
|
2162
|
+
severity: "error",
|
|
2163
|
+
type: "orphaned_task_parents",
|
|
2164
|
+
message: `${orphanedParents} tasks reference missing parent tasks`,
|
|
2165
|
+
count: orphanedParents,
|
|
2166
|
+
repairable: true
|
|
2167
|
+
});
|
|
2168
|
+
}
|
|
2169
|
+
const orphanedDependencies = tableExists(db, "task_dependencies") && tableExists(db, "tasks") ? countQuery(db, "SELECT COUNT(*) as count FROM task_dependencies d WHERE NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = d.task_id) OR NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = d.depends_on)") : 0;
|
|
2170
|
+
if (orphanedDependencies > 0) {
|
|
2171
|
+
addCheck(checks, {
|
|
2172
|
+
severity: "error",
|
|
2173
|
+
type: "orphaned_task_dependencies",
|
|
2174
|
+
message: `${orphanedDependencies} dependency rows reference missing tasks`,
|
|
2175
|
+
count: orphanedDependencies,
|
|
2176
|
+
repairable: true
|
|
2177
|
+
});
|
|
2178
|
+
}
|
|
2179
|
+
const orphanTables = [
|
|
2180
|
+
["task_comments", "NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = task_comments.task_id)"],
|
|
2181
|
+
["task_runs", "NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = task_runs.task_id)"],
|
|
2182
|
+
["task_run_events", "NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = task_run_events.task_id) OR NOT EXISTS (SELECT 1 FROM task_runs r WHERE r.id = task_run_events.run_id)"],
|
|
2183
|
+
["task_run_commands", "NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = task_run_commands.task_id) OR NOT EXISTS (SELECT 1 FROM task_runs r WHERE r.id = task_run_commands.run_id)"],
|
|
2184
|
+
["task_run_artifacts", "NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = task_run_artifacts.task_id) OR NOT EXISTS (SELECT 1 FROM task_runs r WHERE r.id = task_run_artifacts.run_id)"]
|
|
2185
|
+
];
|
|
2186
|
+
let orphanedRows = 0;
|
|
2187
|
+
for (const [table, where] of orphanTables) {
|
|
2188
|
+
if (!tableExists(db, table))
|
|
2189
|
+
continue;
|
|
2190
|
+
orphanedRows += countQuery(db, `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`);
|
|
2191
|
+
}
|
|
2192
|
+
if (orphanedRows > 0) {
|
|
2193
|
+
addCheck(checks, {
|
|
2194
|
+
severity: "error",
|
|
2195
|
+
type: "orphaned_child_rows",
|
|
2196
|
+
message: `${orphanedRows} child rows reference missing tasks or runs`,
|
|
2197
|
+
count: orphanedRows,
|
|
2198
|
+
repairable: true
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
2201
|
+
addTaskStateChecks(db, checks);
|
|
2202
|
+
const corruptJson = findCorruptJsonMetadata(db);
|
|
2203
|
+
if (corruptJson.length > 0) {
|
|
2204
|
+
addCheck(checks, {
|
|
2205
|
+
severity: "error",
|
|
2206
|
+
type: "corrupt_json_metadata",
|
|
2207
|
+
message: `${corruptJson.length} metadata values are not valid JSON`,
|
|
2208
|
+
count: corruptJson.length,
|
|
2209
|
+
repairable: true
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
const duplicateIndexes = findDuplicateIndexes(db);
|
|
2213
|
+
if (duplicateIndexes.length > 0) {
|
|
2214
|
+
addCheck(checks, {
|
|
2215
|
+
severity: "warn",
|
|
2216
|
+
type: "duplicate_indexes",
|
|
2217
|
+
message: `${duplicateIndexes.length} duplicate index definitions found`,
|
|
2218
|
+
count: duplicateIndexes.length,
|
|
2219
|
+
repairable: true
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
const missingProjectRoots = findMissingProjectRoots(db);
|
|
2223
|
+
if (missingProjectRoots > 0) {
|
|
2224
|
+
addCheck(checks, {
|
|
2225
|
+
severity: "warn",
|
|
2226
|
+
type: "missing_project_roots",
|
|
2227
|
+
message: `${missingProjectRoots} project paths do not exist on this machine`,
|
|
2228
|
+
count: missingProjectRoots,
|
|
2229
|
+
repairable: false
|
|
2230
|
+
});
|
|
2231
|
+
}
|
|
2232
|
+
const unsafePermissions = databasePermissionsAreUnsafe(dbPath);
|
|
2233
|
+
addCheck(checks, unsafePermissions ? {
|
|
2234
|
+
severity: "warn",
|
|
2235
|
+
type: "database_permissions",
|
|
2236
|
+
message: "Database file is readable or writable by group/others",
|
|
2237
|
+
repairable: true
|
|
2238
|
+
} : {
|
|
2239
|
+
severity: "info",
|
|
2240
|
+
type: "database_permissions",
|
|
2241
|
+
message: "Database file permissions are private"
|
|
2242
|
+
});
|
|
2243
|
+
let backup;
|
|
2244
|
+
const hasRepairableIssue = checks.some((check) => check.repairable && check.severity !== "info");
|
|
2245
|
+
if (apply && hasRepairableIssue) {
|
|
2246
|
+
backup = createBackup(dbPath);
|
|
2247
|
+
if (backup)
|
|
2248
|
+
pushRepair(repairs, "backup_created", `Created backup at ${backup.path}`, true, backup.files.length);
|
|
2249
|
+
else
|
|
2250
|
+
pushRepair(repairs, "backup_created", "Backup skipped for in-memory or missing database path", false, 0);
|
|
2251
|
+
if (migrationCurrent < migrationExpected || missingTables.length > 0) {
|
|
2252
|
+
runMigrations(db);
|
|
2253
|
+
ensureSchema(db);
|
|
2254
|
+
pushRepair(repairs, "schema_repair", "Ran migration and schema safety net", true);
|
|
2255
|
+
}
|
|
2256
|
+
if (orphanedParents > 0) {
|
|
2257
|
+
db.run("UPDATE tasks SET parent_id = NULL WHERE parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM tasks p WHERE p.id = tasks.parent_id)");
|
|
2258
|
+
pushRepair(repairs, "orphaned_task_parents", "Cleared missing parent references", true, orphanedParents);
|
|
2259
|
+
}
|
|
2260
|
+
if (orphanedDependencies > 0 && tableExists(db, "task_dependencies")) {
|
|
2261
|
+
const count = deleteOrphans(db, "task_dependencies", "NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = task_dependencies.task_id) OR NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = task_dependencies.depends_on)");
|
|
2262
|
+
pushRepair(repairs, "orphaned_task_dependencies", "Deleted dependency rows referencing missing tasks", true, count);
|
|
2263
|
+
}
|
|
2264
|
+
for (const [table, where] of orphanTables) {
|
|
2265
|
+
if (!tableExists(db, table))
|
|
2266
|
+
continue;
|
|
2267
|
+
const count = deleteOrphans(db, table, where);
|
|
2268
|
+
if (count > 0)
|
|
2269
|
+
pushRepair(repairs, "orphaned_child_rows", `Deleted orphaned rows from ${table}`, true, count);
|
|
2270
|
+
}
|
|
2271
|
+
if (corruptJson.length > 0) {
|
|
2272
|
+
for (const cell of corruptJson) {
|
|
2273
|
+
db.run(`UPDATE ${quoteIdent(cell.table)} SET ${quoteIdent(cell.column)} = '{}' WHERE rowid = ?`, [cell.rowid]);
|
|
2274
|
+
}
|
|
2275
|
+
pushRepair(repairs, "corrupt_json_metadata", "Reset invalid metadata JSON values to {}", true, corruptJson.length);
|
|
2276
|
+
}
|
|
2277
|
+
if (duplicateIndexes.length > 0) {
|
|
2278
|
+
let dropped = 0;
|
|
2279
|
+
for (const duplicate of duplicateIndexes) {
|
|
2280
|
+
db.run(`DROP INDEX IF EXISTS ${quoteIdent(duplicate.duplicate)}`);
|
|
2281
|
+
dropped++;
|
|
2282
|
+
}
|
|
2283
|
+
pushRepair(repairs, "duplicate_indexes", "Dropped duplicate non-primary indexes", true, dropped);
|
|
2284
|
+
}
|
|
2285
|
+
if (unsafePermissions) {
|
|
2286
|
+
try {
|
|
2287
|
+
chmodSync(dbPath, 384);
|
|
2288
|
+
pushRepair(repairs, "database_permissions", "Changed database file mode to 0600", true);
|
|
2289
|
+
} catch (error) {
|
|
2290
|
+
pushRepair(repairs, "database_permissions", error instanceof Error ? error.message : "Failed to repair database permissions", false);
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
const finalChecks = apply && hasRepairableIssue ? runTodosDoctor({ db, dbPath, apply: false }).checks : checks;
|
|
2295
|
+
const summary = summarize2(finalChecks, repairs);
|
|
2296
|
+
return {
|
|
2297
|
+
ok: !finalChecks.some((check) => check.severity === "error"),
|
|
2298
|
+
dry_run: !apply,
|
|
2299
|
+
database_path: dbPath,
|
|
2300
|
+
migration: { current: getMigrationLevel(db), expected: migrationExpected },
|
|
2301
|
+
backup,
|
|
2302
|
+
checks: finalChecks,
|
|
2303
|
+
repairs,
|
|
2304
|
+
summary
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
var REQUIRED_TABLES;
|
|
2308
|
+
var init_doctor = __esm(() => {
|
|
2309
|
+
init_database();
|
|
2310
|
+
init_migrations();
|
|
2311
|
+
init_schema();
|
|
2312
|
+
init_recurrence();
|
|
2313
|
+
REQUIRED_TABLES = [
|
|
2314
|
+
"_migrations",
|
|
2315
|
+
"projects",
|
|
2316
|
+
"tasks",
|
|
2317
|
+
"plans",
|
|
2318
|
+
"agents",
|
|
2319
|
+
"task_dependencies",
|
|
2320
|
+
"task_comments",
|
|
2321
|
+
"task_runs",
|
|
2322
|
+
"task_run_events",
|
|
2323
|
+
"task_run_commands",
|
|
2324
|
+
"task_run_artifacts"
|
|
2325
|
+
];
|
|
2326
|
+
});
|
|
2327
|
+
|
|
1519
2328
|
// src/lib/package-version.ts
|
|
1520
2329
|
import { existsSync, readFileSync } from "fs";
|
|
1521
2330
|
import { dirname, join } from "path";
|
|
@@ -1541,8 +2350,8 @@ function getPackageVersion(fromUrl = import.meta.url) {
|
|
|
1541
2350
|
|
|
1542
2351
|
// src/server/serve.ts
|
|
1543
2352
|
init_database();
|
|
1544
|
-
import { existsSync as
|
|
1545
|
-
import { join as
|
|
2353
|
+
import { existsSync as existsSync6 } from "fs";
|
|
2354
|
+
import { join as join6, dirname as dirname6, extname } from "path";
|
|
1546
2355
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1547
2356
|
|
|
1548
2357
|
// src/db/api-keys.ts
|
|
@@ -1806,6 +2615,457 @@ function checkCompletionGuard(task, agentId, db, configOverride) {
|
|
|
1806
2615
|
}
|
|
1807
2616
|
}
|
|
1808
2617
|
|
|
2618
|
+
// src/lib/event-hooks.ts
|
|
2619
|
+
import { createHash as createHash2, randomUUID } from "crypto";
|
|
2620
|
+
import { appendFileSync, mkdirSync as mkdirSync3 } from "fs";
|
|
2621
|
+
import { dirname as dirname4, resolve as resolve4 } from "path";
|
|
2622
|
+
import { createConnection } from "net";
|
|
2623
|
+
|
|
2624
|
+
// src/lib/redaction.ts
|
|
2625
|
+
function redactEvidenceText(value) {
|
|
2626
|
+
return value.replace(/\b(AKIA|ASIA)[0-9A-Z]{16}\b/g, "[REDACTED_AWS_KEY]").replace(/-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g, "[REDACTED_PRIVATE_KEY]").replace(/\bsk-[A-Za-z0-9_-]{12,}\b/g, "[REDACTED_TOKEN]").replace(/\b([A-Za-z0-9_]*(?:API_KEY|TOKEN|SECRET|PASSWORD)[A-Za-z0-9_]*)\s*=\s*['"]?[^'"\s]{8,}/gi, "$1=[REDACTED]").replace(/\b(bearer)\s+[A-Za-z0-9._~+/=-]{12,}/gi, "$1 [REDACTED]");
|
|
2627
|
+
}
|
|
2628
|
+
function redactValue(value) {
|
|
2629
|
+
if (typeof value === "string")
|
|
2630
|
+
return redactEvidenceText(value);
|
|
2631
|
+
if (Array.isArray(value))
|
|
2632
|
+
return value.map(redactValue);
|
|
2633
|
+
if (value && typeof value === "object") {
|
|
2634
|
+
const redacted = {};
|
|
2635
|
+
for (const [key, child] of Object.entries(value)) {
|
|
2636
|
+
if (/api[_-]?key|token|secret|password/i.test(key)) {
|
|
2637
|
+
redacted[key] = "[REDACTED]";
|
|
2638
|
+
} else {
|
|
2639
|
+
redacted[key] = redactValue(child);
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
return redacted;
|
|
2643
|
+
}
|
|
2644
|
+
return value;
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
// src/lib/runner-sandbox.ts
|
|
2648
|
+
import { relative as relative2, resolve as resolve3 } from "path";
|
|
2649
|
+
|
|
2650
|
+
// src/lib/workspace-trust.ts
|
|
2651
|
+
import { relative, resolve as resolve2 } from "path";
|
|
2652
|
+
var DEFAULT_DENYLIST = ["rm -rf", "mkfs", "dd if=", "curl | sh", "wget | sh"];
|
|
2653
|
+
var DEFAULT_ENV_REDACTIONS = ["API_KEY", "TOKEN", "SECRET", "PASSWORD", "AUTH"];
|
|
2654
|
+
var PRESET_DEFAULTS = {
|
|
2655
|
+
restricted: {
|
|
2656
|
+
trusted: false,
|
|
2657
|
+
preset: "restricted",
|
|
2658
|
+
command_allowlist: ["todos"],
|
|
2659
|
+
command_denylist: DEFAULT_DENYLIST,
|
|
2660
|
+
tool_permissions: ["read"],
|
|
2661
|
+
write_scopes: [],
|
|
2662
|
+
env_redactions: DEFAULT_ENV_REDACTIONS,
|
|
2663
|
+
require_prompt_for_unsafe: true
|
|
2664
|
+
},
|
|
2665
|
+
readonly: {
|
|
2666
|
+
trusted: false,
|
|
2667
|
+
preset: "readonly",
|
|
2668
|
+
command_allowlist: ["todos", "git status", "git diff", "bun test"],
|
|
2669
|
+
command_denylist: DEFAULT_DENYLIST,
|
|
2670
|
+
tool_permissions: ["read", "list", "search"],
|
|
2671
|
+
write_scopes: [],
|
|
2672
|
+
env_redactions: DEFAULT_ENV_REDACTIONS,
|
|
2673
|
+
require_prompt_for_unsafe: true
|
|
2674
|
+
},
|
|
2675
|
+
standard: {
|
|
2676
|
+
trusted: true,
|
|
2677
|
+
preset: "standard",
|
|
2678
|
+
command_allowlist: ["todos", "git", "bun", "rg"],
|
|
2679
|
+
command_denylist: DEFAULT_DENYLIST,
|
|
2680
|
+
tool_permissions: ["read", "write", "test", "mcp"],
|
|
2681
|
+
write_scopes: ["."],
|
|
2682
|
+
env_redactions: DEFAULT_ENV_REDACTIONS,
|
|
2683
|
+
require_prompt_for_unsafe: true
|
|
2684
|
+
},
|
|
2685
|
+
trusted: {
|
|
2686
|
+
trusted: true,
|
|
2687
|
+
preset: "trusted",
|
|
2688
|
+
command_allowlist: ["*"],
|
|
2689
|
+
command_denylist: DEFAULT_DENYLIST,
|
|
2690
|
+
tool_permissions: ["*"],
|
|
2691
|
+
write_scopes: ["."],
|
|
2692
|
+
env_redactions: DEFAULT_ENV_REDACTIONS,
|
|
2693
|
+
require_prompt_for_unsafe: false
|
|
2694
|
+
}
|
|
2695
|
+
};
|
|
2696
|
+
function normalizePath(path) {
|
|
2697
|
+
return resolve2(path);
|
|
2698
|
+
}
|
|
2699
|
+
function unique(values) {
|
|
2700
|
+
return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
|
|
2701
|
+
}
|
|
2702
|
+
function defaultProfile(root, preset) {
|
|
2703
|
+
return {
|
|
2704
|
+
root,
|
|
2705
|
+
...PRESET_DEFAULTS[preset]
|
|
2706
|
+
};
|
|
2707
|
+
}
|
|
2708
|
+
function configuredProfiles(config = loadConfig()) {
|
|
2709
|
+
return Object.values(config.workspace_trust || {}).map((profile) => ({ ...profile, root: normalizePath(profile.root) })).sort((a, b) => b.root.length - a.root.length);
|
|
2710
|
+
}
|
|
2711
|
+
function isPathInside(root, path) {
|
|
2712
|
+
const rel = relative(root, path);
|
|
2713
|
+
return rel === "" || !rel.startsWith("..") && !rel.startsWith("/") && !/^[A-Za-z]:/.test(rel);
|
|
2714
|
+
}
|
|
2715
|
+
function matchesPattern(value, pattern) {
|
|
2716
|
+
if (pattern === "*")
|
|
2717
|
+
return true;
|
|
2718
|
+
if (pattern.includes("*")) {
|
|
2719
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
2720
|
+
return new RegExp(`^${escaped}$`, "i").test(value);
|
|
2721
|
+
}
|
|
2722
|
+
return value === pattern || value.startsWith(`${pattern} `) || value.includes(pattern);
|
|
2723
|
+
}
|
|
2724
|
+
function profileFor(path) {
|
|
2725
|
+
const resolved = normalizePath(path);
|
|
2726
|
+
for (const profile of configuredProfiles()) {
|
|
2727
|
+
if (isPathInside(profile.root, resolved))
|
|
2728
|
+
return { profile, matchedRoot: profile.root };
|
|
2729
|
+
}
|
|
2730
|
+
return { profile: defaultProfile(resolved, "restricted"), matchedRoot: null };
|
|
2731
|
+
}
|
|
2732
|
+
function getWorkspaceTrustStatus(path = process.cwd()) {
|
|
2733
|
+
const root = normalizePath(path);
|
|
2734
|
+
const { profile, matchedRoot } = profileFor(root);
|
|
2735
|
+
return {
|
|
2736
|
+
root,
|
|
2737
|
+
trusted: profile.trusted,
|
|
2738
|
+
matched_root: matchedRoot,
|
|
2739
|
+
profile
|
|
2740
|
+
};
|
|
2741
|
+
}
|
|
2742
|
+
function writeAllowed(profile, root, writePath) {
|
|
2743
|
+
const target = normalizePath(writePath.startsWith("/") ? writePath : `${root}/${writePath}`);
|
|
2744
|
+
return profile.write_scopes.some((scope) => {
|
|
2745
|
+
const scopeRoot = normalizePath(scope.startsWith("/") ? scope : `${root}/${scope}`);
|
|
2746
|
+
return isPathInside(scopeRoot, target);
|
|
2747
|
+
});
|
|
2748
|
+
}
|
|
2749
|
+
function redactedEnvKeys(profile, env) {
|
|
2750
|
+
if (!env)
|
|
2751
|
+
return [];
|
|
2752
|
+
const patterns = unique([...DEFAULT_ENV_REDACTIONS, ...profile.env_redactions]).map((item) => item.toUpperCase());
|
|
2753
|
+
return Object.keys(env).filter((key) => patterns.some((pattern) => key.toUpperCase().includes(pattern)));
|
|
2754
|
+
}
|
|
2755
|
+
function checkWorkspacePermission(input = {}) {
|
|
2756
|
+
const status = getWorkspaceTrustStatus(input.path || process.cwd());
|
|
2757
|
+
const reasons = [];
|
|
2758
|
+
const profile = status.profile;
|
|
2759
|
+
if (!status.matched_root)
|
|
2760
|
+
reasons.push("workspace is not trusted");
|
|
2761
|
+
if (input.command) {
|
|
2762
|
+
if (profile.command_denylist.some((pattern) => matchesPattern(input.command, pattern))) {
|
|
2763
|
+
reasons.push("command matches denylist");
|
|
2764
|
+
} else if (!profile.command_allowlist.some((pattern) => matchesPattern(input.command, pattern))) {
|
|
2765
|
+
reasons.push("command is not in allowlist");
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
if (input.tool && !profile.tool_permissions.some((permission) => matchesPattern(input.tool, permission))) {
|
|
2769
|
+
reasons.push("tool permission is not allowed");
|
|
2770
|
+
}
|
|
2771
|
+
if (input.write_path && !writeAllowed(profile, status.matched_root || status.root, input.write_path)) {
|
|
2772
|
+
reasons.push("write path is outside allowed scopes");
|
|
2773
|
+
}
|
|
2774
|
+
const redacted = redactedEnvKeys(profile, input.env);
|
|
2775
|
+
const allowed = reasons.length === 0;
|
|
2776
|
+
return {
|
|
2777
|
+
allowed,
|
|
2778
|
+
requires_prompt: !allowed && profile.require_prompt_for_unsafe,
|
|
2779
|
+
reasons,
|
|
2780
|
+
status,
|
|
2781
|
+
redacted_env_keys: redacted
|
|
2782
|
+
};
|
|
2783
|
+
}
|
|
2784
|
+
|
|
2785
|
+
// src/lib/runner-sandbox.ts
|
|
2786
|
+
var DEFAULT_COMMAND_DENYLIST = ["rm -rf", "mkfs", "dd if=", "curl | sh", "wget | sh"];
|
|
2787
|
+
var DEFAULT_ENV_REDACTIONS2 = ["API_KEY", "TOKEN", "SECRET", "PASSWORD", "AUTH"];
|
|
2788
|
+
function normalizePath2(path) {
|
|
2789
|
+
return resolve3(path);
|
|
2790
|
+
}
|
|
2791
|
+
function unique2(values) {
|
|
2792
|
+
return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
|
|
2793
|
+
}
|
|
2794
|
+
function configuredProfiles2(config = loadConfig()) {
|
|
2795
|
+
return Object.values(config.runner_sandboxes || {}).map((profile) => ({
|
|
2796
|
+
...profile,
|
|
2797
|
+
root: normalizePath2(profile.root),
|
|
2798
|
+
cwd_boundary: normalizePath2(profile.cwd_boundary || profile.root)
|
|
2799
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
2800
|
+
}
|
|
2801
|
+
function isPathInside2(root, path) {
|
|
2802
|
+
const rel = relative2(root, path);
|
|
2803
|
+
return rel === "" || !rel.startsWith("..") && !rel.startsWith("/") && !/^[A-Za-z]:/.test(rel);
|
|
2804
|
+
}
|
|
2805
|
+
function matchesPattern2(value, pattern) {
|
|
2806
|
+
if (pattern === "*")
|
|
2807
|
+
return true;
|
|
2808
|
+
if (pattern.includes("*")) {
|
|
2809
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
2810
|
+
return new RegExp(`^${escaped}$`, "i").test(value);
|
|
2811
|
+
}
|
|
2812
|
+
return value === pattern || value.startsWith(`${pattern} `) || value.includes(pattern);
|
|
2813
|
+
}
|
|
2814
|
+
function resolveFromRoot(root, path) {
|
|
2815
|
+
return normalizePath2(path.startsWith("/") ? path : `${root}/${path}`);
|
|
2816
|
+
}
|
|
2817
|
+
function defaultProfile2(name, root) {
|
|
2818
|
+
const normalizedRoot = normalizePath2(root);
|
|
2819
|
+
return {
|
|
2820
|
+
name,
|
|
2821
|
+
root: normalizedRoot,
|
|
2822
|
+
command_allowlist: ["todos", "git", "bun"],
|
|
2823
|
+
command_denylist: DEFAULT_COMMAND_DENYLIST,
|
|
2824
|
+
cwd_boundary: normalizedRoot,
|
|
2825
|
+
write_scopes: ["."],
|
|
2826
|
+
env_allowlist: ["PATH", "HOME", "SHELL", "TMPDIR", "TEMP", "TMP", "CI", "NODE_ENV", "BUN_ENV"],
|
|
2827
|
+
env_redactions: DEFAULT_ENV_REDACTIONS2,
|
|
2828
|
+
network_policy: "none",
|
|
2829
|
+
require_approval: true,
|
|
2830
|
+
audit_evidence: true
|
|
2831
|
+
};
|
|
2832
|
+
}
|
|
2833
|
+
function profileByName(name, path) {
|
|
2834
|
+
const profiles = configuredProfiles2();
|
|
2835
|
+
if (name) {
|
|
2836
|
+
const found = profiles.find((profile) => profile.name === name);
|
|
2837
|
+
if (found)
|
|
2838
|
+
return found;
|
|
2839
|
+
return defaultProfile2(name, path);
|
|
2840
|
+
}
|
|
2841
|
+
const resolved = normalizePath2(path);
|
|
2842
|
+
return profiles.find((profile) => isPathInside2(profile.root, resolved)) || defaultProfile2("default", resolved);
|
|
2843
|
+
}
|
|
2844
|
+
function redactedEnvKeys2(profile, env) {
|
|
2845
|
+
if (!env)
|
|
2846
|
+
return [];
|
|
2847
|
+
const patterns = unique2([...DEFAULT_ENV_REDACTIONS2, ...profile.env_redactions]).map((item) => item.toUpperCase());
|
|
2848
|
+
return Object.keys(env).filter((key) => patterns.some((pattern) => key.toUpperCase().includes(pattern)));
|
|
2849
|
+
}
|
|
2850
|
+
function omittedEnvKeys(profile, env) {
|
|
2851
|
+
if (!env)
|
|
2852
|
+
return [];
|
|
2853
|
+
if (profile.env_allowlist.includes("*"))
|
|
2854
|
+
return [];
|
|
2855
|
+
return Object.keys(env).filter((key) => !profile.env_allowlist.some((pattern) => matchesPattern2(key, pattern)));
|
|
2856
|
+
}
|
|
2857
|
+
function resolveFromCwd(cwd, path) {
|
|
2858
|
+
return normalizePath2(path.startsWith("/") ? path : `${cwd}/${path}`);
|
|
2859
|
+
}
|
|
2860
|
+
function writeAllowed2(profile, cwd, writePath) {
|
|
2861
|
+
const target = resolveFromCwd(cwd, writePath);
|
|
2862
|
+
return profile.write_scopes.some((scope) => isPathInside2(resolveFromRoot(profile.root, scope), target));
|
|
2863
|
+
}
|
|
2864
|
+
function checkRunnerSandbox(input = {}) {
|
|
2865
|
+
const path = normalizePath2(input.path || input.cwd || process.cwd());
|
|
2866
|
+
const profile = profileByName(input.name, path);
|
|
2867
|
+
const cwd = resolveFromRoot(profile.root, input.cwd || profile.root);
|
|
2868
|
+
const reasons = [];
|
|
2869
|
+
const writePaths = input.write_paths || [];
|
|
2870
|
+
const resolvedWritePaths = writePaths.map((writePath) => resolveFromCwd(cwd, writePath));
|
|
2871
|
+
if (!isPathInside2(profile.cwd_boundary, cwd))
|
|
2872
|
+
reasons.push("cwd is outside sandbox boundary");
|
|
2873
|
+
if (input.command) {
|
|
2874
|
+
if (profile.command_denylist.some((pattern) => matchesPattern2(input.command, pattern))) {
|
|
2875
|
+
reasons.push("command matches sandbox denylist");
|
|
2876
|
+
} else if (!profile.command_allowlist.some((pattern) => matchesPattern2(input.command, pattern))) {
|
|
2877
|
+
reasons.push("command is not in sandbox allowlist");
|
|
2878
|
+
}
|
|
2879
|
+
}
|
|
2880
|
+
for (const writePath of writePaths) {
|
|
2881
|
+
if (!writeAllowed2(profile, cwd, writePath)) {
|
|
2882
|
+
reasons.push(`write path is outside sandbox scopes: ${writePath}`);
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
if (input.network && profile.network_policy === "none") {
|
|
2886
|
+
reasons.push("network access is disabled by sandbox policy");
|
|
2887
|
+
}
|
|
2888
|
+
const trustChecks = [
|
|
2889
|
+
checkWorkspacePermission({ path: profile.root, command: input.command, env: input.env }),
|
|
2890
|
+
...resolvedWritePaths.map((writePath) => checkWorkspacePermission({ path: profile.root, write_path: writePath }))
|
|
2891
|
+
];
|
|
2892
|
+
for (const trust of trustChecks) {
|
|
2893
|
+
for (const reason of trust.reasons)
|
|
2894
|
+
reasons.push(`workspace trust: ${reason}`);
|
|
2895
|
+
}
|
|
2896
|
+
const redacted = redactedEnvKeys2(profile, input.env);
|
|
2897
|
+
const omitted = omittedEnvKeys(profile, input.env);
|
|
2898
|
+
const effective = Object.keys(input.env || {}).filter((key) => !omitted.includes(key));
|
|
2899
|
+
const uniqueReasons = unique2(reasons);
|
|
2900
|
+
const allowed = uniqueReasons.length === 0;
|
|
2901
|
+
return {
|
|
2902
|
+
allowed,
|
|
2903
|
+
requires_approval: !allowed && profile.require_approval,
|
|
2904
|
+
reasons: uniqueReasons,
|
|
2905
|
+
profile,
|
|
2906
|
+
redacted_env_keys: redacted,
|
|
2907
|
+
omitted_env_keys: omitted,
|
|
2908
|
+
effective_env_keys: effective,
|
|
2909
|
+
audit_evidence: profile.audit_evidence ? {
|
|
2910
|
+
sandbox: profile.name,
|
|
2911
|
+
root: profile.root,
|
|
2912
|
+
cwd,
|
|
2913
|
+
command: input.command,
|
|
2914
|
+
write_paths: writePaths,
|
|
2915
|
+
network_requested: Boolean(input.network),
|
|
2916
|
+
network_policy: profile.network_policy,
|
|
2917
|
+
allowed,
|
|
2918
|
+
reasons: uniqueReasons
|
|
2919
|
+
} : null
|
|
2920
|
+
};
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
// src/lib/event-hooks.ts
|
|
2924
|
+
var VALID_TARGETS = new Set(["stdout", "file", "socket", "script"]);
|
|
2925
|
+
function clampAttempts(value) {
|
|
2926
|
+
if (!Number.isFinite(value))
|
|
2927
|
+
return 1;
|
|
2928
|
+
return Math.min(5, Math.max(1, Math.trunc(value)));
|
|
2929
|
+
}
|
|
2930
|
+
function eventMatches(hook, eventType) {
|
|
2931
|
+
return hook.enabled !== false && (hook.events.includes("*") || hook.events.includes(eventType));
|
|
2932
|
+
}
|
|
2933
|
+
function canonicalEvent(input) {
|
|
2934
|
+
return JSON.stringify(input);
|
|
2935
|
+
}
|
|
2936
|
+
function buildEnvelope(type, payload, timestamp = new Date().toISOString()) {
|
|
2937
|
+
const base = {
|
|
2938
|
+
id: randomUUID(),
|
|
2939
|
+
type,
|
|
2940
|
+
timestamp,
|
|
2941
|
+
payload: redactValue(payload ?? {}),
|
|
2942
|
+
source: { package: "@hasna/todos", local_only: true }
|
|
2943
|
+
};
|
|
2944
|
+
const digest = createHash2("sha256").update(canonicalEvent(base)).digest("hex");
|
|
2945
|
+
return { ...base, integrity: { algorithm: "sha256", digest } };
|
|
2946
|
+
}
|
|
2947
|
+
function summarize(value) {
|
|
2948
|
+
const redacted = redactEvidenceText(value.trim());
|
|
2949
|
+
if (!redacted)
|
|
2950
|
+
return;
|
|
2951
|
+
return redacted.length > 1000 ? `${redacted.slice(0, 997)}...` : redacted;
|
|
2952
|
+
}
|
|
2953
|
+
function sleep(ms) {
|
|
2954
|
+
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
2955
|
+
}
|
|
2956
|
+
async function writeSocket(socketPath, line) {
|
|
2957
|
+
await new Promise((resolveWrite, rejectWrite) => {
|
|
2958
|
+
const socket = createConnection(socketPath);
|
|
2959
|
+
const timeout = setTimeout(() => {
|
|
2960
|
+
socket.destroy();
|
|
2961
|
+
rejectWrite(new Error(`socket write timed out: ${socketPath}`));
|
|
2962
|
+
}, 1000);
|
|
2963
|
+
socket.on("error", (error) => {
|
|
2964
|
+
clearTimeout(timeout);
|
|
2965
|
+
rejectWrite(error);
|
|
2966
|
+
});
|
|
2967
|
+
socket.on("connect", () => {
|
|
2968
|
+
socket.end(line, () => {
|
|
2969
|
+
clearTimeout(timeout);
|
|
2970
|
+
resolveWrite();
|
|
2971
|
+
});
|
|
2972
|
+
});
|
|
2973
|
+
});
|
|
2974
|
+
}
|
|
2975
|
+
async function deliverScript(hook, envelope) {
|
|
2976
|
+
const command = hook.command;
|
|
2977
|
+
const cwd = hook.cwd || process.cwd();
|
|
2978
|
+
if (hook.sandbox) {
|
|
2979
|
+
const check = checkRunnerSandbox({ name: hook.sandbox, cwd, command, env: hook.env });
|
|
2980
|
+
if (!check.allowed)
|
|
2981
|
+
throw new Error(check.reasons.join("; "));
|
|
2982
|
+
}
|
|
2983
|
+
const proc = Bun.spawn(["bash", "-lc", command], {
|
|
2984
|
+
cwd,
|
|
2985
|
+
env: {
|
|
2986
|
+
...process.env,
|
|
2987
|
+
...hook.env || {},
|
|
2988
|
+
TODOS_EVENT_JSON: JSON.stringify(envelope),
|
|
2989
|
+
TODOS_EVENT_ID: envelope.id,
|
|
2990
|
+
TODOS_EVENT_TYPE: envelope.type,
|
|
2991
|
+
TODOS_EVENT_INTEGRITY: envelope.integrity.digest,
|
|
2992
|
+
TODOS_HOOK_NAME: hook.name
|
|
2993
|
+
},
|
|
2994
|
+
stdout: "pipe",
|
|
2995
|
+
stderr: "pipe"
|
|
2996
|
+
});
|
|
2997
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
2998
|
+
new Response(proc.stdout).text(),
|
|
2999
|
+
new Response(proc.stderr).text(),
|
|
3000
|
+
proc.exited
|
|
3001
|
+
]);
|
|
3002
|
+
return { exitCode, output: summarize([stdout, stderr].filter(Boolean).join(`
|
|
3003
|
+
`)) };
|
|
3004
|
+
}
|
|
3005
|
+
async function deliverHook(hook, envelope) {
|
|
3006
|
+
const line = `${JSON.stringify(envelope)}
|
|
3007
|
+
`;
|
|
3008
|
+
const maxAttempts = clampAttempts(hook.retry?.attempts ?? 1);
|
|
3009
|
+
const backoffMs = Math.max(0, hook.retry?.backoff_ms ?? 0);
|
|
3010
|
+
let lastError;
|
|
3011
|
+
let output;
|
|
3012
|
+
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
3013
|
+
try {
|
|
3014
|
+
if (hook.target === "stdout") {
|
|
3015
|
+
output = line.trim();
|
|
3016
|
+
} else if (hook.target === "file") {
|
|
3017
|
+
const filePath = resolve4(hook.file_path);
|
|
3018
|
+
mkdirSync3(dirname4(filePath), { recursive: true });
|
|
3019
|
+
appendFileSync(filePath, line);
|
|
3020
|
+
} else if (hook.target === "socket") {
|
|
3021
|
+
await writeSocket(hook.socket_path, line);
|
|
3022
|
+
} else {
|
|
3023
|
+
const result = await deliverScript(hook, envelope);
|
|
3024
|
+
output = result.output;
|
|
3025
|
+
if (result.exitCode !== 0)
|
|
3026
|
+
throw new Error(`script exited ${result.exitCode}${output ? `: ${output}` : ""}`);
|
|
3027
|
+
}
|
|
3028
|
+
return {
|
|
3029
|
+
hook: hook.name,
|
|
3030
|
+
event_id: envelope.id,
|
|
3031
|
+
event_type: envelope.type,
|
|
3032
|
+
target: hook.target,
|
|
3033
|
+
status: "delivered",
|
|
3034
|
+
attempts: attempt,
|
|
3035
|
+
integrity: envelope.integrity,
|
|
3036
|
+
output_summary: output
|
|
3037
|
+
};
|
|
3038
|
+
} catch (error) {
|
|
3039
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
3040
|
+
if (attempt < maxAttempts && backoffMs > 0)
|
|
3041
|
+
await sleep(backoffMs);
|
|
3042
|
+
}
|
|
3043
|
+
}
|
|
3044
|
+
return {
|
|
3045
|
+
hook: hook.name,
|
|
3046
|
+
event_id: envelope.id,
|
|
3047
|
+
event_type: envelope.type,
|
|
3048
|
+
target: hook.target,
|
|
3049
|
+
status: "failed",
|
|
3050
|
+
attempts: maxAttempts,
|
|
3051
|
+
integrity: envelope.integrity,
|
|
3052
|
+
error: redactEvidenceText(lastError || "delivery failed")
|
|
3053
|
+
};
|
|
3054
|
+
}
|
|
3055
|
+
function listLocalEventHooks() {
|
|
3056
|
+
return Object.values(loadConfig().local_event_hooks || {}).sort((a, b) => a.name.localeCompare(b.name));
|
|
3057
|
+
}
|
|
3058
|
+
async function emitLocalEventHooks(input) {
|
|
3059
|
+
const hooks = (input.hooks || listLocalEventHooks()).filter((hook) => eventMatches(hook, input.type));
|
|
3060
|
+
if (hooks.length === 0)
|
|
3061
|
+
return [];
|
|
3062
|
+
const envelope = buildEnvelope(input.type, input.payload, input.timestamp);
|
|
3063
|
+
return Promise.all(hooks.map((hook) => deliverHook(hook, envelope)));
|
|
3064
|
+
}
|
|
3065
|
+
function emitLocalEventHooksQuiet(input) {
|
|
3066
|
+
emitLocalEventHooks(input).catch(() => {});
|
|
3067
|
+
}
|
|
3068
|
+
|
|
1809
3069
|
// src/db/audit.ts
|
|
1810
3070
|
init_database();
|
|
1811
3071
|
function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
|
|
@@ -2439,9 +3699,14 @@ function updateTask(id, input, db) {
|
|
|
2439
3699
|
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
2440
3700
|
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
|
|
2441
3701
|
dispatchWebhook("task.assigned", { id, assigned_to: input.assigned_to, title: task.title }, d).catch(() => {});
|
|
3702
|
+
emitLocalEventHooksQuiet({ type: "task.assigned", payload: { id, assigned_to: input.assigned_to, title: task.title } });
|
|
2442
3703
|
}
|
|
2443
3704
|
if (input.status !== undefined && input.status !== task.status) {
|
|
2444
3705
|
dispatchWebhook("task.status_changed", { id, old_status: task.status, new_status: input.status, title: task.title }, d).catch(() => {});
|
|
3706
|
+
emitLocalEventHooksQuiet({ type: "task.status_changed", payload: { id, old_status: task.status, new_status: input.status, title: task.title } });
|
|
3707
|
+
}
|
|
3708
|
+
if (input.approved_by !== undefined) {
|
|
3709
|
+
emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
|
|
2445
3710
|
}
|
|
2446
3711
|
return {
|
|
2447
3712
|
...task,
|
|
@@ -2468,93 +3733,7 @@ function deleteTask(id, db) {
|
|
|
2468
3733
|
}
|
|
2469
3734
|
// src/db/task-lifecycle.ts
|
|
2470
3735
|
init_database();
|
|
2471
|
-
|
|
2472
|
-
// src/lib/recurrence.ts
|
|
2473
|
-
var DAY_NAMES = {
|
|
2474
|
-
sunday: 0,
|
|
2475
|
-
sun: 0,
|
|
2476
|
-
monday: 1,
|
|
2477
|
-
mon: 1,
|
|
2478
|
-
tuesday: 2,
|
|
2479
|
-
tue: 2,
|
|
2480
|
-
wednesday: 3,
|
|
2481
|
-
wed: 3,
|
|
2482
|
-
thursday: 4,
|
|
2483
|
-
thu: 4,
|
|
2484
|
-
friday: 5,
|
|
2485
|
-
fri: 5,
|
|
2486
|
-
saturday: 6,
|
|
2487
|
-
sat: 6
|
|
2488
|
-
};
|
|
2489
|
-
function parseRecurrenceRule(rule) {
|
|
2490
|
-
const normalized = rule.trim().toLowerCase();
|
|
2491
|
-
if (normalized === "every weekday" || normalized === "every weekdays") {
|
|
2492
|
-
return { type: "specific_days", days: [1, 2, 3, 4, 5] };
|
|
2493
|
-
}
|
|
2494
|
-
if (normalized === "every day" || normalized === "daily") {
|
|
2495
|
-
return { type: "interval", interval: 1, unit: "day" };
|
|
2496
|
-
}
|
|
2497
|
-
if (normalized === "every week" || normalized === "weekly") {
|
|
2498
|
-
return { type: "interval", interval: 1, unit: "week" };
|
|
2499
|
-
}
|
|
2500
|
-
if (normalized === "every month" || normalized === "monthly") {
|
|
2501
|
-
return { type: "interval", interval: 1, unit: "month" };
|
|
2502
|
-
}
|
|
2503
|
-
const intervalMatch = normalized.match(/^every\s+(\d+)\s+(day|week|month)s?$/);
|
|
2504
|
-
if (intervalMatch) {
|
|
2505
|
-
return {
|
|
2506
|
-
type: "interval",
|
|
2507
|
-
interval: parseInt(intervalMatch[1], 10),
|
|
2508
|
-
unit: intervalMatch[2]
|
|
2509
|
-
};
|
|
2510
|
-
}
|
|
2511
|
-
const daysMatch = normalized.match(/^every\s+(.+)$/);
|
|
2512
|
-
if (daysMatch) {
|
|
2513
|
-
const dayParts = daysMatch[1].split(/[,\s]+/).map((d) => d.trim()).filter(Boolean);
|
|
2514
|
-
const days = [];
|
|
2515
|
-
for (const part of dayParts) {
|
|
2516
|
-
const dayNum = DAY_NAMES[part];
|
|
2517
|
-
if (dayNum !== undefined) {
|
|
2518
|
-
days.push(dayNum);
|
|
2519
|
-
}
|
|
2520
|
-
}
|
|
2521
|
-
if (days.length > 0) {
|
|
2522
|
-
return { type: "specific_days", days: days.sort((a, b) => a - b) };
|
|
2523
|
-
}
|
|
2524
|
-
}
|
|
2525
|
-
throw new Error(`Invalid recurrence rule: "${rule}". Supported formats: "every day", "every weekday", "every week", "every 2 weeks", "every month", "every N days/weeks/months", "every monday", "every mon,wed,fri"`);
|
|
2526
|
-
}
|
|
2527
|
-
function nextOccurrence(rule, from) {
|
|
2528
|
-
const parsed = parseRecurrenceRule(rule);
|
|
2529
|
-
const base = from || new Date;
|
|
2530
|
-
if (parsed.type === "interval") {
|
|
2531
|
-
const next = new Date(base);
|
|
2532
|
-
if (parsed.unit === "day") {
|
|
2533
|
-
next.setDate(next.getDate() + parsed.interval);
|
|
2534
|
-
} else if (parsed.unit === "week") {
|
|
2535
|
-
next.setDate(next.getDate() + parsed.interval * 7);
|
|
2536
|
-
} else if (parsed.unit === "month") {
|
|
2537
|
-
next.setMonth(next.getMonth() + parsed.interval);
|
|
2538
|
-
}
|
|
2539
|
-
return next.toISOString();
|
|
2540
|
-
}
|
|
2541
|
-
if (parsed.type === "specific_days") {
|
|
2542
|
-
const currentDay = base.getDay();
|
|
2543
|
-
const days = parsed.days;
|
|
2544
|
-
let daysToAdd = Infinity;
|
|
2545
|
-
for (const day of days) {
|
|
2546
|
-
let diff = day - currentDay;
|
|
2547
|
-
if (diff <= 0)
|
|
2548
|
-
diff += 7;
|
|
2549
|
-
if (diff < daysToAdd)
|
|
2550
|
-
daysToAdd = diff;
|
|
2551
|
-
}
|
|
2552
|
-
const next = new Date(base);
|
|
2553
|
-
next.setDate(next.getDate() + daysToAdd);
|
|
2554
|
-
return next.toISOString();
|
|
2555
|
-
}
|
|
2556
|
-
throw new Error(`Cannot calculate next occurrence for rule: "${rule}"`);
|
|
2557
|
-
}
|
|
3736
|
+
init_recurrence();
|
|
2558
3737
|
|
|
2559
3738
|
// src/db/templates.ts
|
|
2560
3739
|
init_database();
|
|
@@ -2681,6 +3860,13 @@ function getTaskDependencies(taskId, db) {
|
|
|
2681
3860
|
|
|
2682
3861
|
// src/db/task-lifecycle.ts
|
|
2683
3862
|
var MAX_SPAWN_DEPTH = 10;
|
|
3863
|
+
function assertStartable(task, agentId) {
|
|
3864
|
+
if (task.status === "pending")
|
|
3865
|
+
return;
|
|
3866
|
+
if (task.status === "in_progress")
|
|
3867
|
+
return;
|
|
3868
|
+
throw new Error(`Task is ${task.status} and cannot be started by ${agentId}`);
|
|
3869
|
+
}
|
|
2684
3870
|
function getBlockingDeps(id, db) {
|
|
2685
3871
|
const d = db || getDatabase();
|
|
2686
3872
|
const deps = getTaskDependencies(id, d);
|
|
@@ -2699,22 +3885,38 @@ function startTask(id, agentId, db) {
|
|
|
2699
3885
|
const task = getTask(id, d);
|
|
2700
3886
|
if (!task)
|
|
2701
3887
|
throw new TaskNotFoundError(id);
|
|
3888
|
+
assertStartable(task, agentId);
|
|
2702
3889
|
const blocking = getBlockingDeps(id, d);
|
|
2703
3890
|
if (blocking.length > 0) {
|
|
2704
3891
|
const blockerIds = blocking.map((b) => b.id.slice(0, 8)).join(", ");
|
|
3892
|
+
emitLocalEventHooksQuiet({
|
|
3893
|
+
type: "task.blocked",
|
|
3894
|
+
payload: {
|
|
3895
|
+
id,
|
|
3896
|
+
agent_id: agentId,
|
|
3897
|
+
title: task.title,
|
|
3898
|
+
blockers: blocking.map((b) => ({ id: b.id, short_id: b.short_id, title: b.title, status: b.status }))
|
|
3899
|
+
}
|
|
3900
|
+
});
|
|
2705
3901
|
throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
|
|
2706
3902
|
}
|
|
2707
3903
|
const cutoff = lockExpiryCutoff();
|
|
2708
3904
|
const timestamp = now();
|
|
2709
3905
|
const result = d.run(`UPDATE tasks SET status = 'in_progress', assigned_to = ?, locked_by = ?, locked_at = ?, started_at = COALESCE(started_at, ?), version = version + 1, updated_at = ?
|
|
2710
|
-
WHERE id = ? AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp, timestamp, timestamp, id, agentId, cutoff]);
|
|
3906
|
+
WHERE id = ? AND status IN ('pending', 'in_progress') AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp, timestamp, timestamp, id, agentId, cutoff]);
|
|
2711
3907
|
if (result.changes === 0) {
|
|
2712
|
-
|
|
2713
|
-
|
|
3908
|
+
const current = getTask(id, d);
|
|
3909
|
+
if (!current)
|
|
3910
|
+
throw new TaskNotFoundError(id);
|
|
3911
|
+
assertStartable(current, agentId);
|
|
3912
|
+
if (current.locked_by && current.locked_by !== agentId && !isLockExpired(current.locked_at)) {
|
|
3913
|
+
throw new LockError(id, current.locked_by);
|
|
2714
3914
|
}
|
|
3915
|
+
throw new Error(`Task ${id} could not be started because it changed during claim`);
|
|
2715
3916
|
}
|
|
2716
3917
|
logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
|
|
2717
3918
|
dispatchWebhook("task.started", { id, agent_id: agentId, title: task.title }, d).catch(() => {});
|
|
3919
|
+
emitLocalEventHooksQuiet({ type: "task.started", payload: { id, agent_id: agentId, title: task.title } });
|
|
2718
3920
|
return { ...task, status: "in_progress", assigned_to: agentId, locked_by: agentId, locked_at: timestamp, started_at: task.started_at || timestamp, version: task.version + 1, updated_at: timestamp };
|
|
2719
3921
|
}
|
|
2720
3922
|
function completeTask(id, agentId, db, options) {
|
|
@@ -2752,6 +3954,7 @@ function completeTask(id, agentId, db, options) {
|
|
|
2752
3954
|
tx();
|
|
2753
3955
|
logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
|
|
2754
3956
|
dispatchWebhook("task.completed", { id, agent_id: agentId, title: task.title, completed_at: timestamp }, d).catch(() => {});
|
|
3957
|
+
emitLocalEventHooksQuiet({ type: "task.completed", payload: { id, agent_id: agentId, title: task.title, completed_at: timestamp } });
|
|
2755
3958
|
let spawnedTask = null;
|
|
2756
3959
|
if (task.recurrence_rule && !options?.skip_recurrence) {
|
|
2757
3960
|
spawnedTask = spawnNextRecurrence(task, d);
|
|
@@ -2793,6 +3996,7 @@ function completeTask(id, agentId, db, options) {
|
|
|
2793
3996
|
meta._unblocked = unblockedDeps.map((d2) => ({ id: d2.id, short_id: d2.short_id, title: d2.title }));
|
|
2794
3997
|
for (const dep of unblockedDeps) {
|
|
2795
3998
|
dispatchWebhook("task.unblocked", { id: dep.id, unblocked_by: id, title: dep.title }, d).catch(() => {});
|
|
3999
|
+
emitLocalEventHooksQuiet({ type: "task.unblocked", payload: { id: dep.id, unblocked_by: id, title: dep.title } });
|
|
2796
4000
|
}
|
|
2797
4001
|
}
|
|
2798
4002
|
return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: task.version + 1, updated_at: timestamp, metadata: meta };
|
|
@@ -2905,6 +4109,7 @@ function failTask(id, agentId, reason, options, db) {
|
|
|
2905
4109
|
WHERE id = ?`, [JSON.stringify(meta), timestamp, id]);
|
|
2906
4110
|
logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
|
|
2907
4111
|
dispatchWebhook("task.failed", { id, reason, error_code: options?.error_code, agent_id: agentId, title: task.title }, d).catch(() => {});
|
|
4112
|
+
emitLocalEventHooksQuiet({ type: "task.failed", payload: { id, reason, error_code: options?.error_code, agent_id: agentId, title: task.title } });
|
|
2908
4113
|
const failedTask = {
|
|
2909
4114
|
...task,
|
|
2910
4115
|
status: "failed",
|
|
@@ -2949,21 +4154,23 @@ function failTask(id, agentId, reason, options, db) {
|
|
|
2949
4154
|
}
|
|
2950
4155
|
return { task: failedTask, retryTask };
|
|
2951
4156
|
}
|
|
2952
|
-
function getStaleTasks(
|
|
4157
|
+
function getStaleTasks(staleQuery = 30, filters, db) {
|
|
2953
4158
|
const d = db || getDatabase();
|
|
4159
|
+
const staleMinutes = typeof staleQuery === "number" ? staleQuery : staleQuery.minutes ?? (staleQuery.hours !== undefined ? staleQuery.hours * 60 : 30);
|
|
4160
|
+
const effectiveFilters = typeof staleQuery === "number" ? filters : { project_id: staleQuery.project_id, task_list_id: staleQuery.task_list_id };
|
|
2954
4161
|
const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
|
|
2955
4162
|
const conditions = [
|
|
2956
4163
|
"status = 'in_progress'",
|
|
2957
4164
|
"(updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))"
|
|
2958
4165
|
];
|
|
2959
4166
|
const params = [cutoff, cutoff];
|
|
2960
|
-
if (
|
|
4167
|
+
if (effectiveFilters?.project_id) {
|
|
2961
4168
|
conditions.push("project_id = ?");
|
|
2962
|
-
params.push(
|
|
4169
|
+
params.push(effectiveFilters.project_id);
|
|
2963
4170
|
}
|
|
2964
|
-
if (
|
|
4171
|
+
if (effectiveFilters?.task_list_id) {
|
|
2965
4172
|
conditions.push("task_list_id = ?");
|
|
2966
|
-
params.push(
|
|
4173
|
+
params.push(effectiveFilters.task_list_id);
|
|
2967
4174
|
}
|
|
2968
4175
|
const where = conditions.join(" AND ");
|
|
2969
4176
|
const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at ASC`).all(...params);
|
|
@@ -3540,7 +4747,12 @@ function updatePlan(id, input, db) {
|
|
|
3540
4747
|
}
|
|
3541
4748
|
params.push(id);
|
|
3542
4749
|
d.run(`UPDATE plans SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
3543
|
-
|
|
4750
|
+
const updated = getPlan(id, d);
|
|
4751
|
+
emitLocalEventHooksQuiet({
|
|
4752
|
+
type: "plan.updated",
|
|
4753
|
+
payload: { id, old_status: plan.status, new_status: updated.status, name: updated.name, project_id: updated.project_id }
|
|
4754
|
+
});
|
|
4755
|
+
return updated;
|
|
3544
4756
|
}
|
|
3545
4757
|
function deletePlan(id, db) {
|
|
3546
4758
|
const d = db || getDatabase();
|
|
@@ -3548,9 +4760,6 @@ function deletePlan(id, db) {
|
|
|
3548
4760
|
return result.changes > 0;
|
|
3549
4761
|
}
|
|
3550
4762
|
|
|
3551
|
-
// src/server/routes.ts
|
|
3552
|
-
init_database();
|
|
3553
|
-
|
|
3554
4763
|
// src/db/orgs.ts
|
|
3555
4764
|
init_database();
|
|
3556
4765
|
function rowToOrg(row) {
|
|
@@ -3635,7 +4844,7 @@ function listComments(taskId, db) {
|
|
|
3635
4844
|
}
|
|
3636
4845
|
|
|
3637
4846
|
// src/server/routes.ts
|
|
3638
|
-
import { join as
|
|
4847
|
+
import { join as join5, resolve as resolve5, sep } from "path";
|
|
3639
4848
|
function parseFieldsParam(url) {
|
|
3640
4849
|
const fieldsParam = url.searchParams.get("fields");
|
|
3641
4850
|
return fieldsParam ? fieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
|
|
@@ -4206,16 +5415,8 @@ async function handleBulkDeleteProjects(req, _ctx, json2) {
|
|
|
4206
5415
|
}
|
|
4207
5416
|
}
|
|
4208
5417
|
function handleDoctor(_ctx, json2) {
|
|
4209
|
-
const
|
|
4210
|
-
|
|
4211
|
-
if (staleItems.length > 0)
|
|
4212
|
-
issues.push({ severity: "warn", type: "stale_tasks", message: `${staleItems.length} tasks stuck in_progress >30min`, count: staleItems.length });
|
|
4213
|
-
const withParent = getDatabase().query("SELECT COUNT(*) as c FROM tasks t WHERE t.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM tasks p WHERE p.id = t.parent_id)").get();
|
|
4214
|
-
if (withParent.c > 0)
|
|
4215
|
-
issues.push({ severity: "error", type: "orphaned_parents", message: `${withParent.c} tasks reference non-existent parent IDs`, count: withParent.c });
|
|
4216
|
-
if (issues.length === 0)
|
|
4217
|
-
issues.push({ severity: "info", type: "healthy", message: "No issues found" });
|
|
4218
|
-
return json2({ ok: !issues.some((i) => i.severity === "error"), issues });
|
|
5418
|
+
const { runTodosDoctor: runTodosDoctor2 } = (init_doctor(), __toCommonJS(exports_doctor));
|
|
5419
|
+
return json2(runTodosDoctor2({ apply: false }));
|
|
4219
5420
|
}
|
|
4220
5421
|
function handleReport(_req, url, _ctx, json2) {
|
|
4221
5422
|
const days = parseInt(url.searchParams.get("days") || "7", 10);
|
|
@@ -4343,9 +5544,9 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
|
|
|
4343
5544
|
if (!ctx.dashboardExists || method !== "GET" && method !== "HEAD")
|
|
4344
5545
|
return null;
|
|
4345
5546
|
if (path !== "/") {
|
|
4346
|
-
const filePath =
|
|
4347
|
-
const resolvedFile =
|
|
4348
|
-
const resolvedBase =
|
|
5547
|
+
const filePath = join5(ctx.dashboardDir, path);
|
|
5548
|
+
const resolvedFile = resolve5(filePath);
|
|
5549
|
+
const resolvedBase = resolve5(ctx.dashboardDir);
|
|
4349
5550
|
if (!resolvedFile.startsWith(resolvedBase + sep) && resolvedFile !== resolvedBase) {
|
|
4350
5551
|
return json2({ error: "Forbidden" }, 403);
|
|
4351
5552
|
}
|
|
@@ -4353,7 +5554,7 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
|
|
|
4353
5554
|
if (res2)
|
|
4354
5555
|
return res2;
|
|
4355
5556
|
}
|
|
4356
|
-
const indexPath =
|
|
5557
|
+
const indexPath = join5(ctx.dashboardDir, "index.html");
|
|
4357
5558
|
const res = serveStaticFile2(indexPath);
|
|
4358
5559
|
if (res)
|
|
4359
5560
|
return res;
|
|
@@ -4364,21 +5565,21 @@ function handleStaticFiles(path, method, ctx, json2, serveStaticFile2) {
|
|
|
4364
5565
|
function resolveDashboardDir() {
|
|
4365
5566
|
const candidates = [];
|
|
4366
5567
|
try {
|
|
4367
|
-
const scriptDir =
|
|
4368
|
-
candidates.push(
|
|
4369
|
-
candidates.push(
|
|
5568
|
+
const scriptDir = dirname6(fileURLToPath2(import.meta.url));
|
|
5569
|
+
candidates.push(join6(scriptDir, "..", "dashboard", "dist"));
|
|
5570
|
+
candidates.push(join6(scriptDir, "..", "..", "dashboard", "dist"));
|
|
4370
5571
|
} catch {}
|
|
4371
5572
|
if (process.argv[1]) {
|
|
4372
|
-
const mainDir =
|
|
4373
|
-
candidates.push(
|
|
4374
|
-
candidates.push(
|
|
5573
|
+
const mainDir = dirname6(process.argv[1]);
|
|
5574
|
+
candidates.push(join6(mainDir, "..", "dashboard", "dist"));
|
|
5575
|
+
candidates.push(join6(mainDir, "..", "..", "dashboard", "dist"));
|
|
4375
5576
|
}
|
|
4376
|
-
candidates.push(
|
|
5577
|
+
candidates.push(join6(process.cwd(), "dashboard", "dist"));
|
|
4377
5578
|
for (const candidate of candidates) {
|
|
4378
|
-
if (
|
|
5579
|
+
if (existsSync6(candidate))
|
|
4379
5580
|
return candidate;
|
|
4380
5581
|
}
|
|
4381
|
-
return
|
|
5582
|
+
return join6(process.cwd(), "dashboard", "dist");
|
|
4382
5583
|
}
|
|
4383
5584
|
var MIME_TYPES = {
|
|
4384
5585
|
".html": "text/html; charset=utf-8",
|
|
@@ -4450,7 +5651,7 @@ function json(data, status = 200, headers) {
|
|
|
4450
5651
|
});
|
|
4451
5652
|
}
|
|
4452
5653
|
function serveStaticFile(filePath) {
|
|
4453
|
-
if (!
|
|
5654
|
+
if (!existsSync6(filePath))
|
|
4454
5655
|
return null;
|
|
4455
5656
|
const ext = extname(filePath);
|
|
4456
5657
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
@@ -4529,7 +5730,7 @@ data: ${data}
|
|
|
4529
5730
|
filteredSseClients.delete(client);
|
|
4530
5731
|
}
|
|
4531
5732
|
const dashboardDir = resolveDashboardDir();
|
|
4532
|
-
const dashboardExists =
|
|
5733
|
+
const dashboardExists = existsSync6(dashboardDir);
|
|
4533
5734
|
if (!dashboardExists) {
|
|
4534
5735
|
console.error(`
|
|
4535
5736
|
Dashboard not found at: ${dashboardDir}`);
|