@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/storage.js
CHANGED
|
@@ -974,6 +974,154 @@ var init_migrations = __esm(() => {
|
|
|
974
974
|
CREATE INDEX IF NOT EXISTS idx_api_keys_prefix ON api_keys(prefix);
|
|
975
975
|
CREATE INDEX IF NOT EXISTS idx_api_keys_active ON api_keys(revoked_at, expires_at);
|
|
976
976
|
INSERT OR IGNORE INTO _migrations (id) VALUES (50);
|
|
977
|
+
`,
|
|
978
|
+
`
|
|
979
|
+
CREATE TABLE IF NOT EXISTS task_git_refs (
|
|
980
|
+
id TEXT PRIMARY KEY,
|
|
981
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
982
|
+
ref_type TEXT NOT NULL CHECK(ref_type IN ('branch', 'pull_request')),
|
|
983
|
+
name TEXT NOT NULL,
|
|
984
|
+
url TEXT,
|
|
985
|
+
provider TEXT,
|
|
986
|
+
metadata TEXT DEFAULT '{}',
|
|
987
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
988
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
989
|
+
UNIQUE(task_id, ref_type, name)
|
|
990
|
+
);
|
|
991
|
+
CREATE INDEX IF NOT EXISTS idx_task_git_refs_task ON task_git_refs(task_id);
|
|
992
|
+
CREATE INDEX IF NOT EXISTS idx_task_git_refs_lookup ON task_git_refs(ref_type, name);
|
|
993
|
+
CREATE INDEX IF NOT EXISTS idx_task_git_refs_url ON task_git_refs(url);
|
|
994
|
+
|
|
995
|
+
CREATE TABLE IF NOT EXISTS task_verifications (
|
|
996
|
+
id TEXT PRIMARY KEY,
|
|
997
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
998
|
+
command TEXT NOT NULL,
|
|
999
|
+
status TEXT NOT NULL DEFAULT 'unknown' CHECK(status IN ('passed', 'failed', 'unknown')),
|
|
1000
|
+
output_summary TEXT,
|
|
1001
|
+
artifact_path TEXT,
|
|
1002
|
+
agent_id TEXT,
|
|
1003
|
+
run_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1004
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1005
|
+
);
|
|
1006
|
+
CREATE INDEX IF NOT EXISTS idx_task_verifications_task ON task_verifications(task_id);
|
|
1007
|
+
CREATE INDEX IF NOT EXISTS idx_task_verifications_status ON task_verifications(status);
|
|
1008
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (51);
|
|
1009
|
+
`,
|
|
1010
|
+
`
|
|
1011
|
+
CREATE TABLE IF NOT EXISTS task_runs (
|
|
1012
|
+
id TEXT PRIMARY KEY,
|
|
1013
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1014
|
+
agent_id TEXT,
|
|
1015
|
+
title TEXT,
|
|
1016
|
+
status TEXT NOT NULL DEFAULT 'running' CHECK(status IN ('running', 'completed', 'failed', 'cancelled')),
|
|
1017
|
+
summary TEXT,
|
|
1018
|
+
metadata TEXT DEFAULT '{}',
|
|
1019
|
+
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1020
|
+
completed_at TEXT,
|
|
1021
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1022
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1023
|
+
);
|
|
1024
|
+
CREATE INDEX IF NOT EXISTS idx_task_runs_task ON task_runs(task_id);
|
|
1025
|
+
CREATE INDEX IF NOT EXISTS idx_task_runs_agent ON task_runs(agent_id);
|
|
1026
|
+
CREATE INDEX IF NOT EXISTS idx_task_runs_status ON task_runs(status);
|
|
1027
|
+
CREATE INDEX IF NOT EXISTS idx_task_runs_started ON task_runs(started_at);
|
|
1028
|
+
|
|
1029
|
+
CREATE TABLE IF NOT EXISTS task_run_events (
|
|
1030
|
+
id TEXT PRIMARY KEY,
|
|
1031
|
+
run_id TEXT NOT NULL REFERENCES task_runs(id) ON DELETE CASCADE,
|
|
1032
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1033
|
+
event_type TEXT NOT NULL CHECK(event_type IN ('started', 'progress', 'claim', 'comment', 'command', 'file', 'artifact', 'completed', 'failed', 'cancelled')),
|
|
1034
|
+
message TEXT,
|
|
1035
|
+
data TEXT DEFAULT '{}',
|
|
1036
|
+
agent_id TEXT,
|
|
1037
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1038
|
+
);
|
|
1039
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_events_run ON task_run_events(run_id);
|
|
1040
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_events_task ON task_run_events(task_id);
|
|
1041
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_events_type ON task_run_events(event_type);
|
|
1042
|
+
|
|
1043
|
+
CREATE TABLE IF NOT EXISTS task_run_commands (
|
|
1044
|
+
id TEXT PRIMARY KEY,
|
|
1045
|
+
run_id TEXT NOT NULL REFERENCES task_runs(id) ON DELETE CASCADE,
|
|
1046
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1047
|
+
command TEXT NOT NULL,
|
|
1048
|
+
status TEXT NOT NULL DEFAULT 'unknown' CHECK(status IN ('passed', 'failed', 'unknown')),
|
|
1049
|
+
exit_code INTEGER,
|
|
1050
|
+
output_summary TEXT,
|
|
1051
|
+
artifact_path TEXT,
|
|
1052
|
+
agent_id TEXT,
|
|
1053
|
+
started_at TEXT,
|
|
1054
|
+
completed_at TEXT,
|
|
1055
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1056
|
+
);
|
|
1057
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_commands_run ON task_run_commands(run_id);
|
|
1058
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_commands_task ON task_run_commands(task_id);
|
|
1059
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_commands_status ON task_run_commands(status);
|
|
1060
|
+
|
|
1061
|
+
CREATE TABLE IF NOT EXISTS task_run_artifacts (
|
|
1062
|
+
id TEXT PRIMARY KEY,
|
|
1063
|
+
run_id TEXT NOT NULL REFERENCES task_runs(id) ON DELETE CASCADE,
|
|
1064
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1065
|
+
path TEXT NOT NULL,
|
|
1066
|
+
artifact_type TEXT,
|
|
1067
|
+
description TEXT,
|
|
1068
|
+
size_bytes INTEGER,
|
|
1069
|
+
sha256 TEXT,
|
|
1070
|
+
metadata TEXT DEFAULT '{}',
|
|
1071
|
+
agent_id TEXT,
|
|
1072
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1073
|
+
);
|
|
1074
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_run ON task_run_artifacts(run_id);
|
|
1075
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_task ON task_run_artifacts(task_id);
|
|
1076
|
+
CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_path ON task_run_artifacts(path);
|
|
1077
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (52);
|
|
1078
|
+
`,
|
|
1079
|
+
`
|
|
1080
|
+
CREATE TABLE IF NOT EXISTS inbox_items (
|
|
1081
|
+
id TEXT PRIMARY KEY,
|
|
1082
|
+
task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
|
|
1083
|
+
source_type TEXT NOT NULL CHECK(source_type IN ('pasted_error', 'ci_log', 'git_context', 'github_issue', 'file', 'other')),
|
|
1084
|
+
source_name TEXT,
|
|
1085
|
+
source_url TEXT,
|
|
1086
|
+
title TEXT NOT NULL,
|
|
1087
|
+
body TEXT,
|
|
1088
|
+
fingerprint TEXT NOT NULL UNIQUE,
|
|
1089
|
+
status TEXT NOT NULL DEFAULT 'triaged' CHECK(status IN ('new', 'triaged', 'ignored')),
|
|
1090
|
+
metadata TEXT DEFAULT '{}',
|
|
1091
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1092
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1093
|
+
);
|
|
1094
|
+
CREATE INDEX IF NOT EXISTS idx_inbox_items_task ON inbox_items(task_id);
|
|
1095
|
+
CREATE INDEX IF NOT EXISTS idx_inbox_items_source ON inbox_items(source_type, source_name);
|
|
1096
|
+
CREATE INDEX IF NOT EXISTS idx_inbox_items_status ON inbox_items(status);
|
|
1097
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (53);
|
|
1098
|
+
`,
|
|
1099
|
+
`
|
|
1100
|
+
ALTER TABLE handoffs ADD COLUMN session_id TEXT;
|
|
1101
|
+
ALTER TABLE handoffs ADD COLUMN task_ids TEXT;
|
|
1102
|
+
ALTER TABLE handoffs ADD COLUMN relevant_files TEXT;
|
|
1103
|
+
ALTER TABLE handoffs ADD COLUMN run_ids TEXT;
|
|
1104
|
+
CREATE TABLE IF NOT EXISTS handoff_acknowledgements (
|
|
1105
|
+
handoff_id TEXT NOT NULL REFERENCES handoffs(id) ON DELETE CASCADE,
|
|
1106
|
+
agent_id TEXT NOT NULL,
|
|
1107
|
+
acknowledged_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1108
|
+
PRIMARY KEY (handoff_id, agent_id)
|
|
1109
|
+
);
|
|
1110
|
+
CREATE INDEX IF NOT EXISTS idx_handoff_acks_agent ON handoff_acknowledgements(agent_id, acknowledged_at);
|
|
1111
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (54);
|
|
1112
|
+
`,
|
|
1113
|
+
`
|
|
1114
|
+
CREATE TABLE IF NOT EXISTS saved_search_views (
|
|
1115
|
+
id TEXT PRIMARY KEY,
|
|
1116
|
+
name TEXT NOT NULL UNIQUE,
|
|
1117
|
+
description TEXT,
|
|
1118
|
+
scope TEXT NOT NULL DEFAULT 'tasks' CHECK(scope IN ('all', 'tasks', 'projects', 'plans', 'runs', 'comments')),
|
|
1119
|
+
filters TEXT NOT NULL DEFAULT '{}',
|
|
1120
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1121
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1122
|
+
);
|
|
1123
|
+
CREATE INDEX IF NOT EXISTS idx_saved_search_views_scope ON saved_search_views(scope);
|
|
1124
|
+
INSERT OR IGNORE INTO _migrations (id) VALUES (55);
|
|
977
1125
|
`
|
|
978
1126
|
];
|
|
979
1127
|
});
|
|
@@ -1058,6 +1206,17 @@ function ensureSchema(db) {
|
|
|
1058
1206
|
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1059
1207
|
tag TEXT NOT NULL, PRIMARY KEY (task_id, tag)
|
|
1060
1208
|
)`);
|
|
1209
|
+
ensureTable("task_dependencies", `
|
|
1210
|
+
CREATE TABLE task_dependencies (
|
|
1211
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1212
|
+
depends_on TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1213
|
+
external_project_id TEXT,
|
|
1214
|
+
external_task_id TEXT,
|
|
1215
|
+
PRIMARY KEY (task_id, depends_on),
|
|
1216
|
+
CHECK (task_id != depends_on)
|
|
1217
|
+
)`);
|
|
1218
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_dependencies_task ON task_dependencies(task_id)");
|
|
1219
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_dependencies_depends_on ON task_dependencies(depends_on)");
|
|
1061
1220
|
ensureTable("task_history", `
|
|
1062
1221
|
CREATE TABLE task_history (
|
|
1063
1222
|
id TEXT PRIMARY KEY, task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
@@ -1115,6 +1274,41 @@ function ensureSchema(db) {
|
|
|
1115
1274
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1116
1275
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1117
1276
|
)`);
|
|
1277
|
+
ensureTable("handoffs", `
|
|
1278
|
+
CREATE TABLE handoffs (
|
|
1279
|
+
id TEXT PRIMARY KEY,
|
|
1280
|
+
agent_id TEXT,
|
|
1281
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
1282
|
+
session_id TEXT,
|
|
1283
|
+
summary TEXT NOT NULL,
|
|
1284
|
+
completed TEXT,
|
|
1285
|
+
in_progress TEXT,
|
|
1286
|
+
blockers TEXT,
|
|
1287
|
+
next_steps TEXT,
|
|
1288
|
+
task_ids TEXT,
|
|
1289
|
+
relevant_files TEXT,
|
|
1290
|
+
run_ids TEXT,
|
|
1291
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1292
|
+
)`);
|
|
1293
|
+
ensureTable("handoff_acknowledgements", `
|
|
1294
|
+
CREATE TABLE handoff_acknowledgements (
|
|
1295
|
+
handoff_id TEXT NOT NULL REFERENCES handoffs(id) ON DELETE CASCADE,
|
|
1296
|
+
agent_id TEXT NOT NULL,
|
|
1297
|
+
acknowledged_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1298
|
+
PRIMARY KEY (handoff_id, agent_id)
|
|
1299
|
+
)`);
|
|
1300
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_handoff_acks_agent ON handoff_acknowledgements(agent_id, acknowledged_at)");
|
|
1301
|
+
ensureTable("saved_search_views", `
|
|
1302
|
+
CREATE TABLE saved_search_views (
|
|
1303
|
+
id TEXT PRIMARY KEY,
|
|
1304
|
+
name TEXT NOT NULL UNIQUE,
|
|
1305
|
+
description TEXT,
|
|
1306
|
+
scope TEXT NOT NULL DEFAULT 'tasks' CHECK(scope IN ('all', 'tasks', 'projects', 'plans', 'runs', 'comments')),
|
|
1307
|
+
filters TEXT NOT NULL DEFAULT '{}',
|
|
1308
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1309
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1310
|
+
)`);
|
|
1311
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_saved_search_views_scope ON saved_search_views(scope)");
|
|
1118
1312
|
ensureTable("task_relationships", `
|
|
1119
1313
|
CREATE TABLE task_relationships (
|
|
1120
1314
|
id TEXT PRIMARY KEY,
|
|
@@ -1126,6 +1320,121 @@ function ensureSchema(db) {
|
|
|
1126
1320
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1127
1321
|
CHECK (source_task_id != target_task_id)
|
|
1128
1322
|
)`);
|
|
1323
|
+
ensureTable("task_git_refs", `
|
|
1324
|
+
CREATE TABLE task_git_refs (
|
|
1325
|
+
id TEXT PRIMARY KEY,
|
|
1326
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1327
|
+
ref_type TEXT NOT NULL CHECK(ref_type IN ('branch', 'pull_request')),
|
|
1328
|
+
name TEXT NOT NULL,
|
|
1329
|
+
url TEXT,
|
|
1330
|
+
provider TEXT,
|
|
1331
|
+
metadata TEXT DEFAULT '{}',
|
|
1332
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1333
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1334
|
+
UNIQUE(task_id, ref_type, name)
|
|
1335
|
+
)`);
|
|
1336
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_git_refs_task ON task_git_refs(task_id)");
|
|
1337
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_git_refs_lookup ON task_git_refs(ref_type, name)");
|
|
1338
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_git_refs_url ON task_git_refs(url)");
|
|
1339
|
+
ensureTable("task_verifications", `
|
|
1340
|
+
CREATE TABLE task_verifications (
|
|
1341
|
+
id TEXT PRIMARY KEY,
|
|
1342
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1343
|
+
command TEXT NOT NULL,
|
|
1344
|
+
status TEXT NOT NULL DEFAULT 'unknown' CHECK(status IN ('passed', 'failed', 'unknown')),
|
|
1345
|
+
output_summary TEXT,
|
|
1346
|
+
artifact_path TEXT,
|
|
1347
|
+
agent_id TEXT,
|
|
1348
|
+
run_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1349
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1350
|
+
)`);
|
|
1351
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_verifications_task ON task_verifications(task_id)");
|
|
1352
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_verifications_status ON task_verifications(status)");
|
|
1353
|
+
ensureTable("task_runs", `
|
|
1354
|
+
CREATE TABLE task_runs (
|
|
1355
|
+
id TEXT PRIMARY KEY,
|
|
1356
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1357
|
+
agent_id TEXT,
|
|
1358
|
+
title TEXT,
|
|
1359
|
+
status TEXT NOT NULL DEFAULT 'running' CHECK(status IN ('running', 'completed', 'failed', 'cancelled')),
|
|
1360
|
+
summary TEXT,
|
|
1361
|
+
metadata TEXT DEFAULT '{}',
|
|
1362
|
+
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1363
|
+
completed_at TEXT,
|
|
1364
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1365
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1366
|
+
)`);
|
|
1367
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_runs_task ON task_runs(task_id)");
|
|
1368
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_runs_agent ON task_runs(agent_id)");
|
|
1369
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_runs_status ON task_runs(status)");
|
|
1370
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_runs_started ON task_runs(started_at)");
|
|
1371
|
+
ensureTable("task_run_events", `
|
|
1372
|
+
CREATE TABLE task_run_events (
|
|
1373
|
+
id TEXT PRIMARY KEY,
|
|
1374
|
+
run_id TEXT NOT NULL REFERENCES task_runs(id) ON DELETE CASCADE,
|
|
1375
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1376
|
+
event_type TEXT NOT NULL CHECK(event_type IN ('started', 'progress', 'claim', 'comment', 'command', 'file', 'artifact', 'completed', 'failed', 'cancelled')),
|
|
1377
|
+
message TEXT,
|
|
1378
|
+
data TEXT DEFAULT '{}',
|
|
1379
|
+
agent_id TEXT,
|
|
1380
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1381
|
+
)`);
|
|
1382
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_events_run ON task_run_events(run_id)");
|
|
1383
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_events_task ON task_run_events(task_id)");
|
|
1384
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_events_type ON task_run_events(event_type)");
|
|
1385
|
+
ensureTable("task_run_commands", `
|
|
1386
|
+
CREATE TABLE task_run_commands (
|
|
1387
|
+
id TEXT PRIMARY KEY,
|
|
1388
|
+
run_id TEXT NOT NULL REFERENCES task_runs(id) ON DELETE CASCADE,
|
|
1389
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1390
|
+
command TEXT NOT NULL,
|
|
1391
|
+
status TEXT NOT NULL DEFAULT 'unknown' CHECK(status IN ('passed', 'failed', 'unknown')),
|
|
1392
|
+
exit_code INTEGER,
|
|
1393
|
+
output_summary TEXT,
|
|
1394
|
+
artifact_path TEXT,
|
|
1395
|
+
agent_id TEXT,
|
|
1396
|
+
started_at TEXT,
|
|
1397
|
+
completed_at TEXT,
|
|
1398
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1399
|
+
)`);
|
|
1400
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_commands_run ON task_run_commands(run_id)");
|
|
1401
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_commands_task ON task_run_commands(task_id)");
|
|
1402
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_commands_status ON task_run_commands(status)");
|
|
1403
|
+
ensureTable("task_run_artifacts", `
|
|
1404
|
+
CREATE TABLE task_run_artifacts (
|
|
1405
|
+
id TEXT PRIMARY KEY,
|
|
1406
|
+
run_id TEXT NOT NULL REFERENCES task_runs(id) ON DELETE CASCADE,
|
|
1407
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1408
|
+
path TEXT NOT NULL,
|
|
1409
|
+
artifact_type TEXT,
|
|
1410
|
+
description TEXT,
|
|
1411
|
+
size_bytes INTEGER,
|
|
1412
|
+
sha256 TEXT,
|
|
1413
|
+
metadata TEXT DEFAULT '{}',
|
|
1414
|
+
agent_id TEXT,
|
|
1415
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1416
|
+
)`);
|
|
1417
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_run ON task_run_artifacts(run_id)");
|
|
1418
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_task ON task_run_artifacts(task_id)");
|
|
1419
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_run_artifacts_path ON task_run_artifacts(path)");
|
|
1420
|
+
ensureTable("inbox_items", `
|
|
1421
|
+
CREATE TABLE inbox_items (
|
|
1422
|
+
id TEXT PRIMARY KEY,
|
|
1423
|
+
task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
|
|
1424
|
+
source_type TEXT NOT NULL CHECK(source_type IN ('pasted_error', 'ci_log', 'git_context', 'github_issue', 'file', 'other')),
|
|
1425
|
+
source_name TEXT,
|
|
1426
|
+
source_url TEXT,
|
|
1427
|
+
title TEXT NOT NULL,
|
|
1428
|
+
body TEXT,
|
|
1429
|
+
fingerprint TEXT NOT NULL UNIQUE,
|
|
1430
|
+
status TEXT NOT NULL DEFAULT 'triaged' CHECK(status IN ('new', 'triaged', 'ignored')),
|
|
1431
|
+
metadata TEXT DEFAULT '{}',
|
|
1432
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1433
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1434
|
+
)`);
|
|
1435
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_inbox_items_task ON inbox_items(task_id)");
|
|
1436
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_inbox_items_source ON inbox_items(source_type, source_name)");
|
|
1437
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_inbox_items_status ON inbox_items(status)");
|
|
1129
1438
|
ensureTable("kg_edges", `
|
|
1130
1439
|
CREATE TABLE kg_edges (
|
|
1131
1440
|
id TEXT PRIMARY KEY,
|
|
@@ -1287,6 +1596,10 @@ function ensureSchema(db) {
|
|
|
1287
1596
|
ensureColumn("orgs", "synced_at", "TEXT");
|
|
1288
1597
|
ensureColumn("handoffs", "machine_id", "TEXT");
|
|
1289
1598
|
ensureColumn("handoffs", "synced_at", "TEXT");
|
|
1599
|
+
ensureColumn("handoffs", "session_id", "TEXT");
|
|
1600
|
+
ensureColumn("handoffs", "task_ids", "TEXT");
|
|
1601
|
+
ensureColumn("handoffs", "relevant_files", "TEXT");
|
|
1602
|
+
ensureColumn("handoffs", "run_ids", "TEXT");
|
|
1290
1603
|
ensureColumn("task_checklists", "machine_id", "TEXT");
|
|
1291
1604
|
ensureColumn("project_sources", "machine_id", "TEXT");
|
|
1292
1605
|
ensureColumn("project_sources", "synced_at", "TEXT");
|
|
@@ -1531,6 +1844,7 @@ __export(exports_database, {
|
|
|
1531
1844
|
now: () => now,
|
|
1532
1845
|
lockExpiryCutoff: () => lockExpiryCutoff,
|
|
1533
1846
|
isLockExpired: () => isLockExpired,
|
|
1847
|
+
getDatabasePath: () => getDatabasePath,
|
|
1534
1848
|
getDatabase: () => getDatabase,
|
|
1535
1849
|
closeDatabase: () => closeDatabase,
|
|
1536
1850
|
clearExpiredLocks: () => clearExpiredLocks,
|
|
@@ -1592,6 +1906,9 @@ function getDbPath() {
|
|
|
1592
1906
|
}
|
|
1593
1907
|
return newPath;
|
|
1594
1908
|
}
|
|
1909
|
+
function getDatabasePath() {
|
|
1910
|
+
return getDbPath();
|
|
1911
|
+
}
|
|
1595
1912
|
function ensureDir(filePath) {
|
|
1596
1913
|
if (isInMemoryDb(filePath))
|
|
1597
1914
|
return;
|
|
@@ -1629,12 +1946,12 @@ function now() {
|
|
|
1629
1946
|
function uuid() {
|
|
1630
1947
|
return crypto.randomUUID();
|
|
1631
1948
|
}
|
|
1632
|
-
function isLockExpired(lockedAt) {
|
|
1949
|
+
function isLockExpired(lockedAt, nowMs = Date.now()) {
|
|
1633
1950
|
if (!lockedAt)
|
|
1634
1951
|
return true;
|
|
1635
1952
|
const lockTime = new Date(lockedAt).getTime();
|
|
1636
1953
|
const expiryMs = LOCK_EXPIRY_MINUTES * 60 * 1000;
|
|
1637
|
-
return
|
|
1954
|
+
return nowMs - lockTime > expiryMs;
|
|
1638
1955
|
}
|
|
1639
1956
|
function lockExpiryCutoff(nowMs = Date.now()) {
|
|
1640
1957
|
const expiryMs = LOCK_EXPIRY_MINUTES * 60 * 1000;
|
|
@@ -2108,6 +2425,644 @@ function checkCompletionGuard(task, agentId, db, configOverride) {
|
|
|
2108
2425
|
}
|
|
2109
2426
|
}
|
|
2110
2427
|
|
|
2428
|
+
// src/lib/event-hooks.ts
|
|
2429
|
+
import { createHash, randomUUID } from "crypto";
|
|
2430
|
+
import { appendFileSync, mkdirSync as mkdirSync3 } from "fs";
|
|
2431
|
+
import { dirname as dirname3, resolve as resolve4 } from "path";
|
|
2432
|
+
import { createConnection } from "net";
|
|
2433
|
+
|
|
2434
|
+
// src/lib/redaction.ts
|
|
2435
|
+
function redactEvidenceText(value) {
|
|
2436
|
+
return value.replace(/\b(AKIA|ASIA)[0-9A-Z]{16}\b/g, "[REDACTED_AWS_KEY]").replace(/-----BEGIN (?:RSA |EC |OPENSSH |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |)PRIVATE KEY-----/g, "[REDACTED_PRIVATE_KEY]").replace(/\bsk-[A-Za-z0-9_-]{12,}\b/g, "[REDACTED_TOKEN]").replace(/\b([A-Za-z0-9_]*(?:API_KEY|TOKEN|SECRET|PASSWORD)[A-Za-z0-9_]*)\s*=\s*['"]?[^'"\s]{8,}/gi, "$1=[REDACTED]").replace(/\b(bearer)\s+[A-Za-z0-9._~+/=-]{12,}/gi, "$1 [REDACTED]");
|
|
2437
|
+
}
|
|
2438
|
+
function redactValue(value) {
|
|
2439
|
+
if (typeof value === "string")
|
|
2440
|
+
return redactEvidenceText(value);
|
|
2441
|
+
if (Array.isArray(value))
|
|
2442
|
+
return value.map(redactValue);
|
|
2443
|
+
if (value && typeof value === "object") {
|
|
2444
|
+
const redacted = {};
|
|
2445
|
+
for (const [key, child] of Object.entries(value)) {
|
|
2446
|
+
if (/api[_-]?key|token|secret|password/i.test(key)) {
|
|
2447
|
+
redacted[key] = "[REDACTED]";
|
|
2448
|
+
} else {
|
|
2449
|
+
redacted[key] = redactValue(child);
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
return redacted;
|
|
2453
|
+
}
|
|
2454
|
+
return value;
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
// src/lib/runner-sandbox.ts
|
|
2458
|
+
import { relative as relative2, resolve as resolve3 } from "path";
|
|
2459
|
+
|
|
2460
|
+
// src/lib/workspace-trust.ts
|
|
2461
|
+
import { relative, resolve as resolve2 } from "path";
|
|
2462
|
+
var DEFAULT_DENYLIST = ["rm -rf", "mkfs", "dd if=", "curl | sh", "wget | sh"];
|
|
2463
|
+
var DEFAULT_ENV_REDACTIONS = ["API_KEY", "TOKEN", "SECRET", "PASSWORD", "AUTH"];
|
|
2464
|
+
var PRESET_DEFAULTS = {
|
|
2465
|
+
restricted: {
|
|
2466
|
+
trusted: false,
|
|
2467
|
+
preset: "restricted",
|
|
2468
|
+
command_allowlist: ["todos"],
|
|
2469
|
+
command_denylist: DEFAULT_DENYLIST,
|
|
2470
|
+
tool_permissions: ["read"],
|
|
2471
|
+
write_scopes: [],
|
|
2472
|
+
env_redactions: DEFAULT_ENV_REDACTIONS,
|
|
2473
|
+
require_prompt_for_unsafe: true
|
|
2474
|
+
},
|
|
2475
|
+
readonly: {
|
|
2476
|
+
trusted: false,
|
|
2477
|
+
preset: "readonly",
|
|
2478
|
+
command_allowlist: ["todos", "git status", "git diff", "bun test"],
|
|
2479
|
+
command_denylist: DEFAULT_DENYLIST,
|
|
2480
|
+
tool_permissions: ["read", "list", "search"],
|
|
2481
|
+
write_scopes: [],
|
|
2482
|
+
env_redactions: DEFAULT_ENV_REDACTIONS,
|
|
2483
|
+
require_prompt_for_unsafe: true
|
|
2484
|
+
},
|
|
2485
|
+
standard: {
|
|
2486
|
+
trusted: true,
|
|
2487
|
+
preset: "standard",
|
|
2488
|
+
command_allowlist: ["todos", "git", "bun", "rg"],
|
|
2489
|
+
command_denylist: DEFAULT_DENYLIST,
|
|
2490
|
+
tool_permissions: ["read", "write", "test", "mcp"],
|
|
2491
|
+
write_scopes: ["."],
|
|
2492
|
+
env_redactions: DEFAULT_ENV_REDACTIONS,
|
|
2493
|
+
require_prompt_for_unsafe: true
|
|
2494
|
+
},
|
|
2495
|
+
trusted: {
|
|
2496
|
+
trusted: true,
|
|
2497
|
+
preset: "trusted",
|
|
2498
|
+
command_allowlist: ["*"],
|
|
2499
|
+
command_denylist: DEFAULT_DENYLIST,
|
|
2500
|
+
tool_permissions: ["*"],
|
|
2501
|
+
write_scopes: ["."],
|
|
2502
|
+
env_redactions: DEFAULT_ENV_REDACTIONS,
|
|
2503
|
+
require_prompt_for_unsafe: false
|
|
2504
|
+
}
|
|
2505
|
+
};
|
|
2506
|
+
function normalizePath(path) {
|
|
2507
|
+
return resolve2(path);
|
|
2508
|
+
}
|
|
2509
|
+
function unique(values) {
|
|
2510
|
+
return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
|
|
2511
|
+
}
|
|
2512
|
+
function defaultProfile(root, preset) {
|
|
2513
|
+
return {
|
|
2514
|
+
root,
|
|
2515
|
+
...PRESET_DEFAULTS[preset]
|
|
2516
|
+
};
|
|
2517
|
+
}
|
|
2518
|
+
function configuredProfiles(config = loadConfig()) {
|
|
2519
|
+
return Object.values(config.workspace_trust || {}).map((profile) => ({ ...profile, root: normalizePath(profile.root) })).sort((a, b) => b.root.length - a.root.length);
|
|
2520
|
+
}
|
|
2521
|
+
function isPathInside(root, path) {
|
|
2522
|
+
const rel = relative(root, path);
|
|
2523
|
+
return rel === "" || !rel.startsWith("..") && !rel.startsWith("/") && !/^[A-Za-z]:/.test(rel);
|
|
2524
|
+
}
|
|
2525
|
+
function matchesPattern(value, pattern) {
|
|
2526
|
+
if (pattern === "*")
|
|
2527
|
+
return true;
|
|
2528
|
+
if (pattern.includes("*")) {
|
|
2529
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
2530
|
+
return new RegExp(`^${escaped}$`, "i").test(value);
|
|
2531
|
+
}
|
|
2532
|
+
return value === pattern || value.startsWith(`${pattern} `) || value.includes(pattern);
|
|
2533
|
+
}
|
|
2534
|
+
function profileFor(path) {
|
|
2535
|
+
const resolved = normalizePath(path);
|
|
2536
|
+
for (const profile of configuredProfiles()) {
|
|
2537
|
+
if (isPathInside(profile.root, resolved))
|
|
2538
|
+
return { profile, matchedRoot: profile.root };
|
|
2539
|
+
}
|
|
2540
|
+
return { profile: defaultProfile(resolved, "restricted"), matchedRoot: null };
|
|
2541
|
+
}
|
|
2542
|
+
function listWorkspaceTrustProfiles() {
|
|
2543
|
+
return configuredProfiles();
|
|
2544
|
+
}
|
|
2545
|
+
function getWorkspaceTrustStatus(path = process.cwd()) {
|
|
2546
|
+
const root = normalizePath(path);
|
|
2547
|
+
const { profile, matchedRoot } = profileFor(root);
|
|
2548
|
+
return {
|
|
2549
|
+
root,
|
|
2550
|
+
trusted: profile.trusted,
|
|
2551
|
+
matched_root: matchedRoot,
|
|
2552
|
+
profile
|
|
2553
|
+
};
|
|
2554
|
+
}
|
|
2555
|
+
function upsertWorkspaceTrustProfile(input) {
|
|
2556
|
+
const root = normalizePath(input.root);
|
|
2557
|
+
const config = loadConfig();
|
|
2558
|
+
const existing = config.workspace_trust?.[root];
|
|
2559
|
+
const preset = input.preset || existing?.preset || "standard";
|
|
2560
|
+
const presetChanged = Boolean(existing && input.preset && input.preset !== existing.preset);
|
|
2561
|
+
const base = presetChanged ? defaultProfile(root, preset) : existing || defaultProfile(root, preset);
|
|
2562
|
+
const timestamp = new Date().toISOString();
|
|
2563
|
+
const profile = {
|
|
2564
|
+
...base,
|
|
2565
|
+
...PRESET_DEFAULTS[preset],
|
|
2566
|
+
root,
|
|
2567
|
+
preset,
|
|
2568
|
+
trusted: input.trusted ?? base.trusted ?? PRESET_DEFAULTS[preset].trusted,
|
|
2569
|
+
command_allowlist: unique(input.command_allowlist ?? base.command_allowlist ?? PRESET_DEFAULTS[preset].command_allowlist),
|
|
2570
|
+
command_denylist: unique(input.command_denylist ?? base.command_denylist ?? PRESET_DEFAULTS[preset].command_denylist),
|
|
2571
|
+
tool_permissions: unique(input.tool_permissions ?? base.tool_permissions ?? PRESET_DEFAULTS[preset].tool_permissions),
|
|
2572
|
+
write_scopes: unique(input.write_scopes ?? base.write_scopes ?? PRESET_DEFAULTS[preset].write_scopes),
|
|
2573
|
+
env_redactions: unique(input.env_redactions ?? base.env_redactions ?? PRESET_DEFAULTS[preset].env_redactions),
|
|
2574
|
+
require_prompt_for_unsafe: input.require_prompt_for_unsafe ?? base.require_prompt_for_unsafe ?? PRESET_DEFAULTS[preset].require_prompt_for_unsafe,
|
|
2575
|
+
created_at: existing?.created_at || timestamp,
|
|
2576
|
+
updated_at: timestamp
|
|
2577
|
+
};
|
|
2578
|
+
saveConfig({
|
|
2579
|
+
...config,
|
|
2580
|
+
workspace_trust: {
|
|
2581
|
+
...config.workspace_trust || {},
|
|
2582
|
+
[root]: profile
|
|
2583
|
+
}
|
|
2584
|
+
});
|
|
2585
|
+
return profile;
|
|
2586
|
+
}
|
|
2587
|
+
function removeWorkspaceTrustProfile(root) {
|
|
2588
|
+
const normalized = normalizePath(root);
|
|
2589
|
+
const config = loadConfig();
|
|
2590
|
+
if (!config.workspace_trust?.[normalized])
|
|
2591
|
+
return false;
|
|
2592
|
+
const next = { ...config.workspace_trust };
|
|
2593
|
+
delete next[normalized];
|
|
2594
|
+
saveConfig({ ...config, workspace_trust: next });
|
|
2595
|
+
return true;
|
|
2596
|
+
}
|
|
2597
|
+
function writeAllowed(profile, root, writePath) {
|
|
2598
|
+
const target = normalizePath(writePath.startsWith("/") ? writePath : `${root}/${writePath}`);
|
|
2599
|
+
return profile.write_scopes.some((scope) => {
|
|
2600
|
+
const scopeRoot = normalizePath(scope.startsWith("/") ? scope : `${root}/${scope}`);
|
|
2601
|
+
return isPathInside(scopeRoot, target);
|
|
2602
|
+
});
|
|
2603
|
+
}
|
|
2604
|
+
function redactedEnvKeys(profile, env) {
|
|
2605
|
+
if (!env)
|
|
2606
|
+
return [];
|
|
2607
|
+
const patterns = unique([...DEFAULT_ENV_REDACTIONS, ...profile.env_redactions]).map((item) => item.toUpperCase());
|
|
2608
|
+
return Object.keys(env).filter((key) => patterns.some((pattern) => key.toUpperCase().includes(pattern)));
|
|
2609
|
+
}
|
|
2610
|
+
function checkWorkspacePermission(input = {}) {
|
|
2611
|
+
const status = getWorkspaceTrustStatus(input.path || process.cwd());
|
|
2612
|
+
const reasons = [];
|
|
2613
|
+
const profile = status.profile;
|
|
2614
|
+
if (!status.matched_root)
|
|
2615
|
+
reasons.push("workspace is not trusted");
|
|
2616
|
+
if (input.command) {
|
|
2617
|
+
if (profile.command_denylist.some((pattern) => matchesPattern(input.command, pattern))) {
|
|
2618
|
+
reasons.push("command matches denylist");
|
|
2619
|
+
} else if (!profile.command_allowlist.some((pattern) => matchesPattern(input.command, pattern))) {
|
|
2620
|
+
reasons.push("command is not in allowlist");
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
if (input.tool && !profile.tool_permissions.some((permission) => matchesPattern(input.tool, permission))) {
|
|
2624
|
+
reasons.push("tool permission is not allowed");
|
|
2625
|
+
}
|
|
2626
|
+
if (input.write_path && !writeAllowed(profile, status.matched_root || status.root, input.write_path)) {
|
|
2627
|
+
reasons.push("write path is outside allowed scopes");
|
|
2628
|
+
}
|
|
2629
|
+
const redacted = redactedEnvKeys(profile, input.env);
|
|
2630
|
+
const allowed = reasons.length === 0;
|
|
2631
|
+
return {
|
|
2632
|
+
allowed,
|
|
2633
|
+
requires_prompt: !allowed && profile.require_prompt_for_unsafe,
|
|
2634
|
+
reasons,
|
|
2635
|
+
status,
|
|
2636
|
+
redacted_env_keys: redacted
|
|
2637
|
+
};
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
// src/lib/runner-sandbox.ts
|
|
2641
|
+
var DEFAULT_COMMAND_DENYLIST = ["rm -rf", "mkfs", "dd if=", "curl | sh", "wget | sh"];
|
|
2642
|
+
var DEFAULT_ENV_REDACTIONS2 = ["API_KEY", "TOKEN", "SECRET", "PASSWORD", "AUTH"];
|
|
2643
|
+
function normalizePath2(path) {
|
|
2644
|
+
return resolve3(path);
|
|
2645
|
+
}
|
|
2646
|
+
function unique2(values) {
|
|
2647
|
+
return Array.from(new Set((values || []).map((value) => value.trim()).filter(Boolean)));
|
|
2648
|
+
}
|
|
2649
|
+
function configuredProfiles2(config = loadConfig()) {
|
|
2650
|
+
return Object.values(config.runner_sandboxes || {}).map((profile) => ({
|
|
2651
|
+
...profile,
|
|
2652
|
+
root: normalizePath2(profile.root),
|
|
2653
|
+
cwd_boundary: normalizePath2(profile.cwd_boundary || profile.root)
|
|
2654
|
+
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
2655
|
+
}
|
|
2656
|
+
function isPathInside2(root, path) {
|
|
2657
|
+
const rel = relative2(root, path);
|
|
2658
|
+
return rel === "" || !rel.startsWith("..") && !rel.startsWith("/") && !/^[A-Za-z]:/.test(rel);
|
|
2659
|
+
}
|
|
2660
|
+
function matchesPattern2(value, pattern) {
|
|
2661
|
+
if (pattern === "*")
|
|
2662
|
+
return true;
|
|
2663
|
+
if (pattern.includes("*")) {
|
|
2664
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
2665
|
+
return new RegExp(`^${escaped}$`, "i").test(value);
|
|
2666
|
+
}
|
|
2667
|
+
return value === pattern || value.startsWith(`${pattern} `) || value.includes(pattern);
|
|
2668
|
+
}
|
|
2669
|
+
function resolveFromRoot(root, path) {
|
|
2670
|
+
return normalizePath2(path.startsWith("/") ? path : `${root}/${path}`);
|
|
2671
|
+
}
|
|
2672
|
+
function defaultProfile2(name, root) {
|
|
2673
|
+
const normalizedRoot = normalizePath2(root);
|
|
2674
|
+
return {
|
|
2675
|
+
name,
|
|
2676
|
+
root: normalizedRoot,
|
|
2677
|
+
command_allowlist: ["todos", "git", "bun"],
|
|
2678
|
+
command_denylist: DEFAULT_COMMAND_DENYLIST,
|
|
2679
|
+
cwd_boundary: normalizedRoot,
|
|
2680
|
+
write_scopes: ["."],
|
|
2681
|
+
env_allowlist: ["PATH", "HOME", "SHELL", "TMPDIR", "TEMP", "TMP", "CI", "NODE_ENV", "BUN_ENV"],
|
|
2682
|
+
env_redactions: DEFAULT_ENV_REDACTIONS2,
|
|
2683
|
+
network_policy: "none",
|
|
2684
|
+
require_approval: true,
|
|
2685
|
+
audit_evidence: true
|
|
2686
|
+
};
|
|
2687
|
+
}
|
|
2688
|
+
function profileByName(name, path) {
|
|
2689
|
+
const profiles = configuredProfiles2();
|
|
2690
|
+
if (name) {
|
|
2691
|
+
const found = profiles.find((profile) => profile.name === name);
|
|
2692
|
+
if (found)
|
|
2693
|
+
return found;
|
|
2694
|
+
return defaultProfile2(name, path);
|
|
2695
|
+
}
|
|
2696
|
+
const resolved = normalizePath2(path);
|
|
2697
|
+
return profiles.find((profile) => isPathInside2(profile.root, resolved)) || defaultProfile2("default", resolved);
|
|
2698
|
+
}
|
|
2699
|
+
function redactedEnvKeys2(profile, env) {
|
|
2700
|
+
if (!env)
|
|
2701
|
+
return [];
|
|
2702
|
+
const patterns = unique2([...DEFAULT_ENV_REDACTIONS2, ...profile.env_redactions]).map((item) => item.toUpperCase());
|
|
2703
|
+
return Object.keys(env).filter((key) => patterns.some((pattern) => key.toUpperCase().includes(pattern)));
|
|
2704
|
+
}
|
|
2705
|
+
function omittedEnvKeys(profile, env) {
|
|
2706
|
+
if (!env)
|
|
2707
|
+
return [];
|
|
2708
|
+
if (profile.env_allowlist.includes("*"))
|
|
2709
|
+
return [];
|
|
2710
|
+
return Object.keys(env).filter((key) => !profile.env_allowlist.some((pattern) => matchesPattern2(key, pattern)));
|
|
2711
|
+
}
|
|
2712
|
+
function resolveFromCwd(cwd, path) {
|
|
2713
|
+
return normalizePath2(path.startsWith("/") ? path : `${cwd}/${path}`);
|
|
2714
|
+
}
|
|
2715
|
+
function writeAllowed2(profile, cwd, writePath) {
|
|
2716
|
+
const target = resolveFromCwd(cwd, writePath);
|
|
2717
|
+
return profile.write_scopes.some((scope) => isPathInside2(resolveFromRoot(profile.root, scope), target));
|
|
2718
|
+
}
|
|
2719
|
+
function listRunnerSandboxProfiles() {
|
|
2720
|
+
return configuredProfiles2();
|
|
2721
|
+
}
|
|
2722
|
+
function getRunnerSandboxProfile(name, path = process.cwd()) {
|
|
2723
|
+
return profileByName(name, path);
|
|
2724
|
+
}
|
|
2725
|
+
function upsertRunnerSandboxProfile(input) {
|
|
2726
|
+
const config = loadConfig();
|
|
2727
|
+
const existing = config.runner_sandboxes?.[input.name];
|
|
2728
|
+
const root = normalizePath2(input.root || existing?.root || process.cwd());
|
|
2729
|
+
const base = existing || defaultProfile2(input.name, root);
|
|
2730
|
+
const timestamp = new Date().toISOString();
|
|
2731
|
+
const profile = {
|
|
2732
|
+
...base,
|
|
2733
|
+
name: input.name,
|
|
2734
|
+
root,
|
|
2735
|
+
command_allowlist: unique2(input.command_allowlist ?? base.command_allowlist),
|
|
2736
|
+
command_denylist: unique2(input.command_denylist ?? base.command_denylist),
|
|
2737
|
+
cwd_boundary: normalizePath2(input.cwd_boundary || base.cwd_boundary || root),
|
|
2738
|
+
write_scopes: unique2(input.write_scopes ?? base.write_scopes),
|
|
2739
|
+
env_allowlist: unique2(input.env_allowlist ?? base.env_allowlist),
|
|
2740
|
+
env_redactions: unique2(input.env_redactions ?? base.env_redactions),
|
|
2741
|
+
network_policy: input.network_policy || base.network_policy,
|
|
2742
|
+
require_approval: input.require_approval ?? base.require_approval,
|
|
2743
|
+
audit_evidence: input.audit_evidence ?? base.audit_evidence,
|
|
2744
|
+
created_at: existing?.created_at || timestamp,
|
|
2745
|
+
updated_at: timestamp
|
|
2746
|
+
};
|
|
2747
|
+
saveConfig({
|
|
2748
|
+
...config,
|
|
2749
|
+
runner_sandboxes: {
|
|
2750
|
+
...config.runner_sandboxes || {},
|
|
2751
|
+
[profile.name]: profile
|
|
2752
|
+
}
|
|
2753
|
+
});
|
|
2754
|
+
return profile;
|
|
2755
|
+
}
|
|
2756
|
+
function removeRunnerSandboxProfile(name) {
|
|
2757
|
+
const config = loadConfig();
|
|
2758
|
+
if (!config.runner_sandboxes?.[name])
|
|
2759
|
+
return false;
|
|
2760
|
+
const next = { ...config.runner_sandboxes };
|
|
2761
|
+
delete next[name];
|
|
2762
|
+
saveConfig({ ...config, runner_sandboxes: next });
|
|
2763
|
+
return true;
|
|
2764
|
+
}
|
|
2765
|
+
function checkRunnerSandbox(input = {}) {
|
|
2766
|
+
const path = normalizePath2(input.path || input.cwd || process.cwd());
|
|
2767
|
+
const profile = profileByName(input.name, path);
|
|
2768
|
+
const cwd = resolveFromRoot(profile.root, input.cwd || profile.root);
|
|
2769
|
+
const reasons = [];
|
|
2770
|
+
const writePaths = input.write_paths || [];
|
|
2771
|
+
const resolvedWritePaths = writePaths.map((writePath) => resolveFromCwd(cwd, writePath));
|
|
2772
|
+
if (!isPathInside2(profile.cwd_boundary, cwd))
|
|
2773
|
+
reasons.push("cwd is outside sandbox boundary");
|
|
2774
|
+
if (input.command) {
|
|
2775
|
+
if (profile.command_denylist.some((pattern) => matchesPattern2(input.command, pattern))) {
|
|
2776
|
+
reasons.push("command matches sandbox denylist");
|
|
2777
|
+
} else if (!profile.command_allowlist.some((pattern) => matchesPattern2(input.command, pattern))) {
|
|
2778
|
+
reasons.push("command is not in sandbox allowlist");
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
for (const writePath of writePaths) {
|
|
2782
|
+
if (!writeAllowed2(profile, cwd, writePath)) {
|
|
2783
|
+
reasons.push(`write path is outside sandbox scopes: ${writePath}`);
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
if (input.network && profile.network_policy === "none") {
|
|
2787
|
+
reasons.push("network access is disabled by sandbox policy");
|
|
2788
|
+
}
|
|
2789
|
+
const trustChecks = [
|
|
2790
|
+
checkWorkspacePermission({ path: profile.root, command: input.command, env: input.env }),
|
|
2791
|
+
...resolvedWritePaths.map((writePath) => checkWorkspacePermission({ path: profile.root, write_path: writePath }))
|
|
2792
|
+
];
|
|
2793
|
+
for (const trust of trustChecks) {
|
|
2794
|
+
for (const reason of trust.reasons)
|
|
2795
|
+
reasons.push(`workspace trust: ${reason}`);
|
|
2796
|
+
}
|
|
2797
|
+
const redacted = redactedEnvKeys2(profile, input.env);
|
|
2798
|
+
const omitted = omittedEnvKeys(profile, input.env);
|
|
2799
|
+
const effective = Object.keys(input.env || {}).filter((key) => !omitted.includes(key));
|
|
2800
|
+
const uniqueReasons = unique2(reasons);
|
|
2801
|
+
const allowed = uniqueReasons.length === 0;
|
|
2802
|
+
return {
|
|
2803
|
+
allowed,
|
|
2804
|
+
requires_approval: !allowed && profile.require_approval,
|
|
2805
|
+
reasons: uniqueReasons,
|
|
2806
|
+
profile,
|
|
2807
|
+
redacted_env_keys: redacted,
|
|
2808
|
+
omitted_env_keys: omitted,
|
|
2809
|
+
effective_env_keys: effective,
|
|
2810
|
+
audit_evidence: profile.audit_evidence ? {
|
|
2811
|
+
sandbox: profile.name,
|
|
2812
|
+
root: profile.root,
|
|
2813
|
+
cwd,
|
|
2814
|
+
command: input.command,
|
|
2815
|
+
write_paths: writePaths,
|
|
2816
|
+
network_requested: Boolean(input.network),
|
|
2817
|
+
network_policy: profile.network_policy,
|
|
2818
|
+
allowed,
|
|
2819
|
+
reasons: uniqueReasons
|
|
2820
|
+
} : null
|
|
2821
|
+
};
|
|
2822
|
+
}
|
|
2823
|
+
function explainRunnerSandbox(input = {}) {
|
|
2824
|
+
return checkRunnerSandbox(input);
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
// src/lib/event-hooks.ts
|
|
2828
|
+
var LOCAL_EVENT_TYPES = [
|
|
2829
|
+
"task.assigned",
|
|
2830
|
+
"task.blocked",
|
|
2831
|
+
"task.started",
|
|
2832
|
+
"task.completed",
|
|
2833
|
+
"task.failed",
|
|
2834
|
+
"task.unblocked",
|
|
2835
|
+
"task.status_changed",
|
|
2836
|
+
"plan.updated",
|
|
2837
|
+
"run.started",
|
|
2838
|
+
"run.completed",
|
|
2839
|
+
"run.failed",
|
|
2840
|
+
"run.cancelled",
|
|
2841
|
+
"approval.decided",
|
|
2842
|
+
"import.finished",
|
|
2843
|
+
"export.finished"
|
|
2844
|
+
];
|
|
2845
|
+
var VALID_TARGETS = new Set(["stdout", "file", "socket", "script"]);
|
|
2846
|
+
function safeName(name) {
|
|
2847
|
+
const trimmed = name.trim();
|
|
2848
|
+
if (!trimmed)
|
|
2849
|
+
throw new Error("event hook name is required");
|
|
2850
|
+
if (!/^[a-zA-Z0-9._-]+$/.test(trimmed))
|
|
2851
|
+
throw new Error("event hook name may only contain letters, numbers, dot, underscore, or dash");
|
|
2852
|
+
return trimmed;
|
|
2853
|
+
}
|
|
2854
|
+
function normalizeEvents(events) {
|
|
2855
|
+
const normalized = events.map((event) => event.trim()).filter(Boolean);
|
|
2856
|
+
if (normalized.length === 0)
|
|
2857
|
+
throw new Error("event hook requires at least one event");
|
|
2858
|
+
return Array.from(new Set(normalized)).sort();
|
|
2859
|
+
}
|
|
2860
|
+
function normalizeHook(input, existing) {
|
|
2861
|
+
if (!VALID_TARGETS.has(input.target))
|
|
2862
|
+
throw new Error(`unsupported event hook target: ${input.target}`);
|
|
2863
|
+
if (input.target === "file" && !input.file_path && !existing?.file_path)
|
|
2864
|
+
throw new Error("file event hooks require file_path");
|
|
2865
|
+
if (input.target === "socket" && !input.socket_path && !existing?.socket_path)
|
|
2866
|
+
throw new Error("socket event hooks require socket_path");
|
|
2867
|
+
if (input.target === "script" && !input.command && !existing?.command)
|
|
2868
|
+
throw new Error("script event hooks require command");
|
|
2869
|
+
const timestamp = new Date().toISOString();
|
|
2870
|
+
return {
|
|
2871
|
+
...existing,
|
|
2872
|
+
name: safeName(input.name),
|
|
2873
|
+
enabled: input.enabled ?? existing?.enabled ?? true,
|
|
2874
|
+
events: normalizeEvents(input.events.length > 0 ? input.events : existing?.events || []),
|
|
2875
|
+
target: input.target,
|
|
2876
|
+
file_path: input.file_path ?? existing?.file_path,
|
|
2877
|
+
socket_path: input.socket_path ?? existing?.socket_path,
|
|
2878
|
+
command: input.command ?? existing?.command,
|
|
2879
|
+
cwd: input.cwd ?? existing?.cwd,
|
|
2880
|
+
sandbox: input.sandbox ?? existing?.sandbox,
|
|
2881
|
+
env: input.env ?? existing?.env,
|
|
2882
|
+
retry: {
|
|
2883
|
+
attempts: clampAttempts(input.retry?.attempts ?? existing?.retry?.attempts ?? 1),
|
|
2884
|
+
backoff_ms: Math.max(0, input.retry?.backoff_ms ?? existing?.retry?.backoff_ms ?? 0)
|
|
2885
|
+
},
|
|
2886
|
+
created_at: existing?.created_at || timestamp,
|
|
2887
|
+
updated_at: timestamp
|
|
2888
|
+
};
|
|
2889
|
+
}
|
|
2890
|
+
function clampAttempts(value) {
|
|
2891
|
+
if (!Number.isFinite(value))
|
|
2892
|
+
return 1;
|
|
2893
|
+
return Math.min(5, Math.max(1, Math.trunc(value)));
|
|
2894
|
+
}
|
|
2895
|
+
function eventMatches(hook, eventType) {
|
|
2896
|
+
return hook.enabled !== false && (hook.events.includes("*") || hook.events.includes(eventType));
|
|
2897
|
+
}
|
|
2898
|
+
function canonicalEvent(input) {
|
|
2899
|
+
return JSON.stringify(input);
|
|
2900
|
+
}
|
|
2901
|
+
function buildEnvelope(type, payload, timestamp = new Date().toISOString()) {
|
|
2902
|
+
const base = {
|
|
2903
|
+
id: randomUUID(),
|
|
2904
|
+
type,
|
|
2905
|
+
timestamp,
|
|
2906
|
+
payload: redactValue(payload ?? {}),
|
|
2907
|
+
source: { package: "@hasna/todos", local_only: true }
|
|
2908
|
+
};
|
|
2909
|
+
const digest = createHash("sha256").update(canonicalEvent(base)).digest("hex");
|
|
2910
|
+
return { ...base, integrity: { algorithm: "sha256", digest } };
|
|
2911
|
+
}
|
|
2912
|
+
function summarize(value) {
|
|
2913
|
+
const redacted = redactEvidenceText(value.trim());
|
|
2914
|
+
if (!redacted)
|
|
2915
|
+
return;
|
|
2916
|
+
return redacted.length > 1000 ? `${redacted.slice(0, 997)}...` : redacted;
|
|
2917
|
+
}
|
|
2918
|
+
function sleep(ms) {
|
|
2919
|
+
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
|
|
2920
|
+
}
|
|
2921
|
+
async function writeSocket(socketPath, line) {
|
|
2922
|
+
await new Promise((resolveWrite, rejectWrite) => {
|
|
2923
|
+
const socket = createConnection(socketPath);
|
|
2924
|
+
const timeout = setTimeout(() => {
|
|
2925
|
+
socket.destroy();
|
|
2926
|
+
rejectWrite(new Error(`socket write timed out: ${socketPath}`));
|
|
2927
|
+
}, 1000);
|
|
2928
|
+
socket.on("error", (error) => {
|
|
2929
|
+
clearTimeout(timeout);
|
|
2930
|
+
rejectWrite(error);
|
|
2931
|
+
});
|
|
2932
|
+
socket.on("connect", () => {
|
|
2933
|
+
socket.end(line, () => {
|
|
2934
|
+
clearTimeout(timeout);
|
|
2935
|
+
resolveWrite();
|
|
2936
|
+
});
|
|
2937
|
+
});
|
|
2938
|
+
});
|
|
2939
|
+
}
|
|
2940
|
+
async function deliverScript(hook, envelope) {
|
|
2941
|
+
const command = hook.command;
|
|
2942
|
+
const cwd = hook.cwd || process.cwd();
|
|
2943
|
+
if (hook.sandbox) {
|
|
2944
|
+
const check = checkRunnerSandbox({ name: hook.sandbox, cwd, command, env: hook.env });
|
|
2945
|
+
if (!check.allowed)
|
|
2946
|
+
throw new Error(check.reasons.join("; "));
|
|
2947
|
+
}
|
|
2948
|
+
const proc = Bun.spawn(["bash", "-lc", command], {
|
|
2949
|
+
cwd,
|
|
2950
|
+
env: {
|
|
2951
|
+
...process.env,
|
|
2952
|
+
...hook.env || {},
|
|
2953
|
+
TODOS_EVENT_JSON: JSON.stringify(envelope),
|
|
2954
|
+
TODOS_EVENT_ID: envelope.id,
|
|
2955
|
+
TODOS_EVENT_TYPE: envelope.type,
|
|
2956
|
+
TODOS_EVENT_INTEGRITY: envelope.integrity.digest,
|
|
2957
|
+
TODOS_HOOK_NAME: hook.name
|
|
2958
|
+
},
|
|
2959
|
+
stdout: "pipe",
|
|
2960
|
+
stderr: "pipe"
|
|
2961
|
+
});
|
|
2962
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
2963
|
+
new Response(proc.stdout).text(),
|
|
2964
|
+
new Response(proc.stderr).text(),
|
|
2965
|
+
proc.exited
|
|
2966
|
+
]);
|
|
2967
|
+
return { exitCode, output: summarize([stdout, stderr].filter(Boolean).join(`
|
|
2968
|
+
`)) };
|
|
2969
|
+
}
|
|
2970
|
+
async function deliverHook(hook, envelope) {
|
|
2971
|
+
const line = `${JSON.stringify(envelope)}
|
|
2972
|
+
`;
|
|
2973
|
+
const maxAttempts = clampAttempts(hook.retry?.attempts ?? 1);
|
|
2974
|
+
const backoffMs = Math.max(0, hook.retry?.backoff_ms ?? 0);
|
|
2975
|
+
let lastError;
|
|
2976
|
+
let output;
|
|
2977
|
+
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
2978
|
+
try {
|
|
2979
|
+
if (hook.target === "stdout") {
|
|
2980
|
+
output = line.trim();
|
|
2981
|
+
} else if (hook.target === "file") {
|
|
2982
|
+
const filePath = resolve4(hook.file_path);
|
|
2983
|
+
mkdirSync3(dirname3(filePath), { recursive: true });
|
|
2984
|
+
appendFileSync(filePath, line);
|
|
2985
|
+
} else if (hook.target === "socket") {
|
|
2986
|
+
await writeSocket(hook.socket_path, line);
|
|
2987
|
+
} else {
|
|
2988
|
+
const result = await deliverScript(hook, envelope);
|
|
2989
|
+
output = result.output;
|
|
2990
|
+
if (result.exitCode !== 0)
|
|
2991
|
+
throw new Error(`script exited ${result.exitCode}${output ? `: ${output}` : ""}`);
|
|
2992
|
+
}
|
|
2993
|
+
return {
|
|
2994
|
+
hook: hook.name,
|
|
2995
|
+
event_id: envelope.id,
|
|
2996
|
+
event_type: envelope.type,
|
|
2997
|
+
target: hook.target,
|
|
2998
|
+
status: "delivered",
|
|
2999
|
+
attempts: attempt,
|
|
3000
|
+
integrity: envelope.integrity,
|
|
3001
|
+
output_summary: output
|
|
3002
|
+
};
|
|
3003
|
+
} catch (error) {
|
|
3004
|
+
lastError = error instanceof Error ? error.message : String(error);
|
|
3005
|
+
if (attempt < maxAttempts && backoffMs > 0)
|
|
3006
|
+
await sleep(backoffMs);
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
return {
|
|
3010
|
+
hook: hook.name,
|
|
3011
|
+
event_id: envelope.id,
|
|
3012
|
+
event_type: envelope.type,
|
|
3013
|
+
target: hook.target,
|
|
3014
|
+
status: "failed",
|
|
3015
|
+
attempts: maxAttempts,
|
|
3016
|
+
integrity: envelope.integrity,
|
|
3017
|
+
error: redactEvidenceText(lastError || "delivery failed")
|
|
3018
|
+
};
|
|
3019
|
+
}
|
|
3020
|
+
function upsertLocalEventHook(input) {
|
|
3021
|
+
const config = loadConfig();
|
|
3022
|
+
const existing = config.local_event_hooks?.[input.name];
|
|
3023
|
+
const hook = normalizeHook(input, existing);
|
|
3024
|
+
saveConfig({
|
|
3025
|
+
...config,
|
|
3026
|
+
local_event_hooks: {
|
|
3027
|
+
...config.local_event_hooks || {},
|
|
3028
|
+
[hook.name]: hook
|
|
3029
|
+
}
|
|
3030
|
+
});
|
|
3031
|
+
return hook;
|
|
3032
|
+
}
|
|
3033
|
+
function listLocalEventHooks() {
|
|
3034
|
+
return Object.values(loadConfig().local_event_hooks || {}).sort((a, b) => a.name.localeCompare(b.name));
|
|
3035
|
+
}
|
|
3036
|
+
function getLocalEventHook(name) {
|
|
3037
|
+
return loadConfig().local_event_hooks?.[safeName(name)] || null;
|
|
3038
|
+
}
|
|
3039
|
+
function removeLocalEventHook(name) {
|
|
3040
|
+
const config = loadConfig();
|
|
3041
|
+
const key = safeName(name);
|
|
3042
|
+
if (!config.local_event_hooks?.[key])
|
|
3043
|
+
return false;
|
|
3044
|
+
const next = { ...config.local_event_hooks };
|
|
3045
|
+
delete next[key];
|
|
3046
|
+
saveConfig({ ...config, local_event_hooks: next });
|
|
3047
|
+
return true;
|
|
3048
|
+
}
|
|
3049
|
+
async function emitLocalEventHooks(input) {
|
|
3050
|
+
const hooks = (input.hooks || listLocalEventHooks()).filter((hook) => eventMatches(hook, input.type));
|
|
3051
|
+
if (hooks.length === 0)
|
|
3052
|
+
return [];
|
|
3053
|
+
const envelope = buildEnvelope(input.type, input.payload, input.timestamp);
|
|
3054
|
+
return Promise.all(hooks.map((hook) => deliverHook(hook, envelope)));
|
|
3055
|
+
}
|
|
3056
|
+
function emitLocalEventHooksQuiet(input) {
|
|
3057
|
+
emitLocalEventHooks(input).catch(() => {});
|
|
3058
|
+
}
|
|
3059
|
+
async function testLocalEventHook(name, input) {
|
|
3060
|
+
const hook = getLocalEventHook(name);
|
|
3061
|
+
if (!hook)
|
|
3062
|
+
throw new Error(`event hook not found: ${name}`);
|
|
3063
|
+
return emitLocalEventHooks({ ...input, hooks: [hook] });
|
|
3064
|
+
}
|
|
3065
|
+
|
|
2111
3066
|
// src/db/audit.ts
|
|
2112
3067
|
init_database();
|
|
2113
3068
|
function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
|
|
@@ -2853,9 +3808,14 @@ function updateTask(id, input, db) {
|
|
|
2853
3808
|
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
2854
3809
|
if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to) {
|
|
2855
3810
|
dispatchWebhook("task.assigned", { id, assigned_to: input.assigned_to, title: task.title }, d).catch(() => {});
|
|
3811
|
+
emitLocalEventHooksQuiet({ type: "task.assigned", payload: { id, assigned_to: input.assigned_to, title: task.title } });
|
|
2856
3812
|
}
|
|
2857
3813
|
if (input.status !== undefined && input.status !== task.status) {
|
|
2858
3814
|
dispatchWebhook("task.status_changed", { id, old_status: task.status, new_status: input.status, title: task.title }, d).catch(() => {});
|
|
3815
|
+
emitLocalEventHooksQuiet({ type: "task.status_changed", payload: { id, old_status: task.status, new_status: input.status, title: task.title } });
|
|
3816
|
+
}
|
|
3817
|
+
if (input.approved_by !== undefined) {
|
|
3818
|
+
emitLocalEventHooksQuiet({ type: "approval.decided", payload: { id, approved_by: input.approved_by, title: task.title } });
|
|
2859
3819
|
}
|
|
2860
3820
|
return {
|
|
2861
3821
|
...task,
|
|
@@ -3546,6 +4506,18 @@ function wouldCreateCycle(taskId, dependsOn, db) {
|
|
|
3546
4506
|
|
|
3547
4507
|
// src/db/task-lifecycle.ts
|
|
3548
4508
|
var MAX_SPAWN_DEPTH = 10;
|
|
4509
|
+
function lockExpiresAt(lockedAt) {
|
|
4510
|
+
if (!lockedAt)
|
|
4511
|
+
return null;
|
|
4512
|
+
return new Date(new Date(lockedAt).getTime() + LOCK_EXPIRY_MINUTES * 60 * 1000).toISOString();
|
|
4513
|
+
}
|
|
4514
|
+
function assertStartable(task, agentId) {
|
|
4515
|
+
if (task.status === "pending")
|
|
4516
|
+
return;
|
|
4517
|
+
if (task.status === "in_progress")
|
|
4518
|
+
return;
|
|
4519
|
+
throw new Error(`Task is ${task.status} and cannot be started by ${agentId}`);
|
|
4520
|
+
}
|
|
3549
4521
|
function getBlockingDeps(id, db) {
|
|
3550
4522
|
const d = db || getDatabase();
|
|
3551
4523
|
const deps = getTaskDependencies(id, d);
|
|
@@ -3564,22 +4536,38 @@ function startTask(id, agentId, db) {
|
|
|
3564
4536
|
const task = getTask(id, d);
|
|
3565
4537
|
if (!task)
|
|
3566
4538
|
throw new TaskNotFoundError(id);
|
|
4539
|
+
assertStartable(task, agentId);
|
|
3567
4540
|
const blocking = getBlockingDeps(id, d);
|
|
3568
4541
|
if (blocking.length > 0) {
|
|
3569
4542
|
const blockerIds = blocking.map((b) => b.id.slice(0, 8)).join(", ");
|
|
4543
|
+
emitLocalEventHooksQuiet({
|
|
4544
|
+
type: "task.blocked",
|
|
4545
|
+
payload: {
|
|
4546
|
+
id,
|
|
4547
|
+
agent_id: agentId,
|
|
4548
|
+
title: task.title,
|
|
4549
|
+
blockers: blocking.map((b) => ({ id: b.id, short_id: b.short_id, title: b.title, status: b.status }))
|
|
4550
|
+
}
|
|
4551
|
+
});
|
|
3570
4552
|
throw new Error(`Task is blocked by ${blocking.length} unfinished dependency(ies): ${blockerIds}`);
|
|
3571
4553
|
}
|
|
3572
4554
|
const cutoff = lockExpiryCutoff();
|
|
3573
4555
|
const timestamp = now();
|
|
3574
4556
|
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 = ?
|
|
3575
|
-
WHERE id = ? AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, agentId, timestamp, timestamp, timestamp, id, agentId, cutoff]);
|
|
4557
|
+
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]);
|
|
3576
4558
|
if (result.changes === 0) {
|
|
3577
|
-
|
|
3578
|
-
|
|
4559
|
+
const current = getTask(id, d);
|
|
4560
|
+
if (!current)
|
|
4561
|
+
throw new TaskNotFoundError(id);
|
|
4562
|
+
assertStartable(current, agentId);
|
|
4563
|
+
if (current.locked_by && current.locked_by !== agentId && !isLockExpired(current.locked_at)) {
|
|
4564
|
+
throw new LockError(id, current.locked_by);
|
|
3579
4565
|
}
|
|
4566
|
+
throw new Error(`Task ${id} could not be started because it changed during claim`);
|
|
3580
4567
|
}
|
|
3581
4568
|
logTaskChange(id, "start", "status", "pending", "in_progress", agentId, d);
|
|
3582
4569
|
dispatchWebhook("task.started", { id, agent_id: agentId, title: task.title }, d).catch(() => {});
|
|
4570
|
+
emitLocalEventHooksQuiet({ type: "task.started", payload: { id, agent_id: agentId, title: task.title } });
|
|
3583
4571
|
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 };
|
|
3584
4572
|
}
|
|
3585
4573
|
function completeTask(id, agentId, db, options) {
|
|
@@ -3617,6 +4605,7 @@ function completeTask(id, agentId, db, options) {
|
|
|
3617
4605
|
tx();
|
|
3618
4606
|
logTaskChange(id, "complete", "status", task.status, "completed", agentId || null, d);
|
|
3619
4607
|
dispatchWebhook("task.completed", { id, agent_id: agentId, title: task.title, completed_at: timestamp }, d).catch(() => {});
|
|
4608
|
+
emitLocalEventHooksQuiet({ type: "task.completed", payload: { id, agent_id: agentId, title: task.title, completed_at: timestamp } });
|
|
3620
4609
|
let spawnedTask = null;
|
|
3621
4610
|
if (task.recurrence_rule && !options?.skip_recurrence) {
|
|
3622
4611
|
spawnedTask = spawnNextRecurrence(task, d);
|
|
@@ -3658,6 +4647,7 @@ function completeTask(id, agentId, db, options) {
|
|
|
3658
4647
|
meta._unblocked = unblockedDeps.map((d2) => ({ id: d2.id, short_id: d2.short_id, title: d2.title }));
|
|
3659
4648
|
for (const dep of unblockedDeps) {
|
|
3660
4649
|
dispatchWebhook("task.unblocked", { id: dep.id, unblocked_by: id, title: dep.title }, d).catch(() => {});
|
|
4650
|
+
emitLocalEventHooksQuiet({ type: "task.unblocked", payload: { id: dep.id, unblocked_by: id, title: dep.title } });
|
|
3661
4651
|
}
|
|
3662
4652
|
}
|
|
3663
4653
|
return { ...task, status: "completed", locked_by: null, locked_at: null, completed_at: timestamp, confidence, version: task.version + 1, updated_at: timestamp, metadata: meta };
|
|
@@ -3667,17 +4657,32 @@ function lockTask(id, agentId, db) {
|
|
|
3667
4657
|
const task = getTask(id, d);
|
|
3668
4658
|
if (!task)
|
|
3669
4659
|
throw new TaskNotFoundError(id);
|
|
4660
|
+
if (task.status === "completed" || task.status === "cancelled") {
|
|
4661
|
+
return {
|
|
4662
|
+
success: false,
|
|
4663
|
+
error: `Task is ${task.status} and cannot be locked`
|
|
4664
|
+
};
|
|
4665
|
+
}
|
|
3670
4666
|
if (task.locked_by === agentId && !isLockExpired(task.locked_at)) {
|
|
3671
|
-
|
|
4667
|
+
const timestamp2 = now();
|
|
4668
|
+
d.run(`UPDATE tasks SET locked_at = ?, updated_at = ?, version = version + 1 WHERE id = ? AND locked_by = ?`, [timestamp2, timestamp2, id, agentId]);
|
|
4669
|
+
logTaskChange(id, "lock_renew", "locked_by", agentId, agentId, agentId, d);
|
|
4670
|
+
return { success: true, locked_by: agentId, locked_at: timestamp2, expires_at: lockExpiresAt(timestamp2) };
|
|
3672
4671
|
}
|
|
3673
4672
|
const cutoff = lockExpiryCutoff();
|
|
3674
4673
|
const timestamp = now();
|
|
3675
4674
|
const result = d.run(`UPDATE tasks SET locked_by = ?, locked_at = ?, version = version + 1, updated_at = ?
|
|
3676
|
-
WHERE id = ? AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, timestamp, timestamp, id, agentId, cutoff]);
|
|
4675
|
+
WHERE id = ? AND status NOT IN ('completed', 'cancelled') AND (locked_by IS NULL OR locked_by = ? OR locked_at < ?)`, [agentId, timestamp, timestamp, id, agentId, cutoff]);
|
|
3677
4676
|
if (result.changes === 0) {
|
|
3678
4677
|
const current = getTask(id, d);
|
|
3679
4678
|
if (!current)
|
|
3680
4679
|
throw new TaskNotFoundError(id);
|
|
4680
|
+
if (current.status === "completed" || current.status === "cancelled") {
|
|
4681
|
+
return {
|
|
4682
|
+
success: false,
|
|
4683
|
+
error: `Task is ${current.status} and cannot be locked`
|
|
4684
|
+
};
|
|
4685
|
+
}
|
|
3681
4686
|
if (current.locked_by && !isLockExpired(current.locked_at)) {
|
|
3682
4687
|
return {
|
|
3683
4688
|
success: false,
|
|
@@ -3686,8 +4691,13 @@ function lockTask(id, agentId, db) {
|
|
|
3686
4691
|
error: `Task is locked by ${current.locked_by}`
|
|
3687
4692
|
};
|
|
3688
4693
|
}
|
|
4694
|
+
return {
|
|
4695
|
+
success: false,
|
|
4696
|
+
error: `Task ${id} could not be locked because it changed during lock acquisition`
|
|
4697
|
+
};
|
|
3689
4698
|
}
|
|
3690
|
-
|
|
4699
|
+
logTaskChange(id, "lock", "locked_by", task.locked_by, agentId, agentId, d);
|
|
4700
|
+
return { success: true, locked_by: agentId, locked_at: timestamp, expires_at: lockExpiresAt(timestamp) };
|
|
3691
4701
|
}
|
|
3692
4702
|
function unlockTask(id, agentId, db) {
|
|
3693
4703
|
const d = db || getDatabase();
|
|
@@ -3702,6 +4712,21 @@ function unlockTask(id, agentId, db) {
|
|
|
3702
4712
|
WHERE id = ?`, [timestamp, id]);
|
|
3703
4713
|
return true;
|
|
3704
4714
|
}
|
|
4715
|
+
function getTaskLockStatus(id, db) {
|
|
4716
|
+
const d = db || getDatabase();
|
|
4717
|
+
const task = getTask(id, d);
|
|
4718
|
+
if (!task)
|
|
4719
|
+
throw new TaskNotFoundError(id);
|
|
4720
|
+
const expired = isLockExpired(task.locked_at);
|
|
4721
|
+
return {
|
|
4722
|
+
task_id: id,
|
|
4723
|
+
locked: !!task.locked_by && !expired,
|
|
4724
|
+
locked_by: task.locked_by,
|
|
4725
|
+
locked_at: task.locked_at,
|
|
4726
|
+
expires_at: lockExpiresAt(task.locked_at),
|
|
4727
|
+
expired
|
|
4728
|
+
};
|
|
4729
|
+
}
|
|
3705
4730
|
function claimNextTask(agentId, filters, db) {
|
|
3706
4731
|
const d = db || getDatabase();
|
|
3707
4732
|
const tx = d.transaction(() => {
|
|
@@ -3810,6 +4835,7 @@ function failTask(id, agentId, reason, options, db) {
|
|
|
3810
4835
|
WHERE id = ?`, [JSON.stringify(meta), timestamp, id]);
|
|
3811
4836
|
logTaskChange(id, "fail", "status", task.status, "failed", agentId || null, d);
|
|
3812
4837
|
dispatchWebhook("task.failed", { id, reason, error_code: options?.error_code, agent_id: agentId, title: task.title }, d).catch(() => {});
|
|
4838
|
+
emitLocalEventHooksQuiet({ type: "task.failed", payload: { id, reason, error_code: options?.error_code, agent_id: agentId, title: task.title } });
|
|
3813
4839
|
const failedTask = {
|
|
3814
4840
|
...task,
|
|
3815
4841
|
status: "failed",
|
|
@@ -3854,21 +4880,23 @@ function failTask(id, agentId, reason, options, db) {
|
|
|
3854
4880
|
}
|
|
3855
4881
|
return { task: failedTask, retryTask };
|
|
3856
4882
|
}
|
|
3857
|
-
function getStaleTasks(
|
|
4883
|
+
function getStaleTasks(staleQuery = 30, filters, db) {
|
|
3858
4884
|
const d = db || getDatabase();
|
|
4885
|
+
const staleMinutes = typeof staleQuery === "number" ? staleQuery : staleQuery.minutes ?? (staleQuery.hours !== undefined ? staleQuery.hours * 60 : 30);
|
|
4886
|
+
const effectiveFilters = typeof staleQuery === "number" ? filters : { project_id: staleQuery.project_id, task_list_id: staleQuery.task_list_id };
|
|
3859
4887
|
const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
|
|
3860
4888
|
const conditions = [
|
|
3861
4889
|
"status = 'in_progress'",
|
|
3862
4890
|
"(updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))"
|
|
3863
4891
|
];
|
|
3864
4892
|
const params = [cutoff, cutoff];
|
|
3865
|
-
if (
|
|
4893
|
+
if (effectiveFilters?.project_id) {
|
|
3866
4894
|
conditions.push("project_id = ?");
|
|
3867
|
-
params.push(
|
|
4895
|
+
params.push(effectiveFilters.project_id);
|
|
3868
4896
|
}
|
|
3869
|
-
if (
|
|
4897
|
+
if (effectiveFilters?.task_list_id) {
|
|
3870
4898
|
conditions.push("task_list_id = ?");
|
|
3871
|
-
params.push(
|
|
4899
|
+
params.push(effectiveFilters.task_list_id);
|
|
3872
4900
|
}
|
|
3873
4901
|
const where = conditions.join(" AND ");
|
|
3874
4902
|
const rows = d.query(`SELECT * FROM tasks WHERE ${where} ORDER BY updated_at ASC`).all(...params);
|
|
@@ -3884,9 +4912,15 @@ function stealTask(agentId, opts, db) {
|
|
|
3884
4912
|
staleTasks.sort((a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9));
|
|
3885
4913
|
const target = staleTasks[0];
|
|
3886
4914
|
const timestamp = now();
|
|
3887
|
-
|
|
4915
|
+
const cutoff = new Date(Date.now() - staleMinutes * 60 * 1000).toISOString();
|
|
4916
|
+
const result = d.run(`UPDATE tasks SET assigned_to = ?, locked_by = ?, locked_at = ?, updated_at = ?, version = version + 1
|
|
4917
|
+
WHERE id = ? AND status = 'in_progress' AND (updated_at < ? OR (locked_at IS NOT NULL AND locked_at < ?))`, [agentId, agentId, timestamp, timestamp, target.id, cutoff, cutoff]);
|
|
4918
|
+
if (result.changes === 0)
|
|
4919
|
+
return null;
|
|
3888
4920
|
logTaskChange(target.id, "steal", "assigned_to", target.assigned_to, agentId, agentId, d);
|
|
4921
|
+
logTaskChange(target.id, "steal", "locked_by", target.locked_by, agentId, agentId, d);
|
|
3889
4922
|
dispatchWebhook("task.assigned", { id: target.id, agent_id: agentId, title: target.title, stolen_from: target.assigned_to }, d).catch(() => {});
|
|
4923
|
+
emitLocalEventHooksQuiet({ type: "task.assigned", payload: { id: target.id, agent_id: agentId, title: target.title, stolen_from: target.assigned_to } });
|
|
3890
4924
|
return { ...target, assigned_to: agentId, locked_by: agentId, locked_at: timestamp, updated_at: timestamp, version: target.version + 1 };
|
|
3891
4925
|
}
|
|
3892
4926
|
function claimOrSteal(agentId, filters, db) {
|
|
@@ -4253,7 +5287,12 @@ function updatePlan(id, input, db) {
|
|
|
4253
5287
|
}
|
|
4254
5288
|
params.push(id);
|
|
4255
5289
|
d.run(`UPDATE plans SET ${sets.join(", ")} WHERE id = ?`, params);
|
|
4256
|
-
|
|
5290
|
+
const updated = getPlan(id, d);
|
|
5291
|
+
emitLocalEventHooksQuiet({
|
|
5292
|
+
type: "plan.updated",
|
|
5293
|
+
payload: { id, old_status: plan.status, new_status: updated.status, name: updated.name, project_id: updated.project_id }
|
|
5294
|
+
});
|
|
5295
|
+
return updated;
|
|
4257
5296
|
}
|
|
4258
5297
|
function deletePlan(id, db) {
|
|
4259
5298
|
const d = db || getDatabase();
|