@hasna/todos 0.11.52 → 0.11.53
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 +14 -1
- package/dashboard/dist/assets/{index-C3fBxEWP.js → index-aJefI7kh.js} +1 -1
- package/dashboard/dist/index.html +1 -1
- package/dist/cli/commands/config-serve-commands.d.ts.map +1 -1
- package/dist/cli/commands/machines.d.ts.map +1 -1
- package/dist/cli/commands/storage-commands.d.ts +3 -0
- package/dist/cli/commands/storage-commands.d.ts.map +1 -0
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/helpers.d.ts +6 -0
- package/dist/cli/helpers.d.ts.map +1 -1
- package/dist/cli/index.js +3306 -445
- package/dist/contracts.js +289 -8
- package/dist/db/artifacts.d.ts.map +1 -1
- package/dist/db/builtin-templates.d.ts +0 -1
- package/dist/db/builtin-templates.d.ts.map +1 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/tags.d.ts +26 -0
- package/dist/db/tags.d.ts.map +1 -0
- package/dist/db/task-commits.d.ts +14 -6
- package/dist/db/task-commits.d.ts.map +1 -1
- package/dist/db/tasks.d.ts +1 -1
- package/dist/db/tasks.d.ts.map +1 -1
- package/dist/index.d.ts +32 -60
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2473 -200
- package/dist/lib/agent-adapter-docs.d.ts +1 -1
- package/dist/lib/agent-adapter-docs.d.ts.map +1 -1
- package/dist/lib/agent-coordination.d.ts.map +1 -1
- package/dist/lib/agent-run-dispatcher.d.ts +5 -2
- package/dist/lib/agent-run-dispatcher.d.ts.map +1 -1
- package/dist/lib/agent-workflow-demo.d.ts +1 -1
- package/dist/lib/agent-workflow-demo.d.ts.map +1 -1
- package/dist/lib/db-backup.d.ts.map +1 -1
- package/dist/lib/headless-boundaries.d.ts +2 -2
- package/dist/lib/headless-boundaries.d.ts.map +1 -1
- package/dist/lib/import-export-bridge.d.ts.map +1 -1
- package/dist/lib/inbox-intake.d.ts.map +1 -1
- package/dist/lib/issue-importers.d.ts.map +1 -1
- package/dist/lib/machine-topology.d.ts +1 -1
- package/dist/lib/machine-topology.d.ts.map +1 -1
- package/dist/lib/native-storage-status.d.ts +65 -0
- package/dist/lib/native-storage-status.d.ts.map +1 -0
- package/dist/lib/nl-intake.d.ts.map +1 -1
- package/dist/lib/notification-reminders.d.ts.map +1 -1
- package/dist/lib/plan-execution.d.ts +1 -0
- package/dist/lib/plan-execution.d.ts.map +1 -1
- package/dist/lib/release-checks.d.ts.map +1 -1
- package/dist/lib/resource-snapshots.d.ts.map +1 -1
- package/dist/lib/saved-views.d.ts.map +1 -1
- package/dist/lib/secret-redaction.d.ts +1 -1
- package/dist/lib/secret-redaction.d.ts.map +1 -1
- package/dist/lib/task-scheduling.d.ts +1 -1
- package/dist/lib/task-scheduling.d.ts.map +1 -1
- package/dist/lib/template-library.d.ts +1 -1
- package/dist/lib/template-library.d.ts.map +1 -1
- package/dist/lib/user-scaffolds.d.ts.map +1 -1
- package/dist/lib/verification-evidence.d.ts +4 -4
- package/dist/lib/verification-evidence.d.ts.map +1 -1
- package/dist/lib/verification-providers.d.ts +3 -3
- package/dist/lib/verification-providers.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +526 -50
- package/dist/mcp/tools/task-crud.d.ts.map +1 -1
- package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
- package/dist/registry.js +289 -8
- package/dist/release-provenance.json +7 -0
- package/dist/server/index.js +552 -52
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/storage/config.d.ts +86 -0
- package/dist/storage/config.d.ts.map +1 -0
- package/dist/storage/factory.d.ts +17 -0
- package/dist/storage/factory.d.ts.map +1 -0
- package/dist/storage/hybrid.d.ts +26 -0
- package/dist/storage/hybrid.d.ts.map +1 -0
- package/dist/storage/index.d.ts +15 -0
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/local-sqlite.d.ts.map +1 -1
- package/dist/storage/postgres-adapter.d.ts +11 -0
- package/dist/storage/postgres-adapter.d.ts.map +1 -0
- package/dist/storage/postgres-sync.d.ts +44 -0
- package/dist/storage/postgres-sync.d.ts.map +1 -0
- package/dist/storage/s3-artifact-sync.d.ts +75 -0
- package/dist/storage/s3-artifact-sync.d.ts.map +1 -0
- package/dist/storage/s3-artifacts.d.ts +51 -0
- package/dist/storage/s3-artifacts.d.ts.map +1 -0
- package/dist/storage/sqlite-snapshot.d.ts +5 -0
- package/dist/storage/sqlite-snapshot.d.ts.map +1 -0
- package/dist/storage.d.ts +2 -1
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +2053 -10
- package/package.json +1 -1
- package/dist/cli/brains.d.ts +0 -3
- package/dist/cli/brains.d.ts.map +0 -1
- package/dist/db/file-locks.d.ts +0 -43
- package/dist/db/file-locks.d.ts.map +0 -1
- package/dist/db/project-agent-roles.d.ts +0 -34
- package/dist/db/project-agent-roles.d.ts.map +0 -1
- package/dist/db/task-claim.d.ts +0 -7
- package/dist/db/task-claim.d.ts.map +0 -1
- package/dist/db/task-workflow.d.ts +0 -7
- package/dist/db/task-workflow.d.ts.map +0 -1
- package/dist/lib/north-star.d.ts +0 -33
- package/dist/lib/north-star.d.ts.map +0 -1
- package/dist/lib/public-release-gate.d.ts +0 -57
- package/dist/lib/public-release-gate.d.ts.map +0 -1
- package/dist/lib/task-runner.d.ts +0 -101
- package/dist/lib/task-runner.d.ts.map +0 -1
- package/dist/mcp/tools/access-profiles.d.ts +0 -8
- package/dist/mcp/tools/access-profiles.d.ts.map +0 -1
- package/dist/mcp/tools/activity-audit.d.ts +0 -9
- package/dist/mcp/tools/activity-audit.d.ts.map +0 -1
- package/dist/mcp/tools/agent-adapter-docs.d.ts +0 -8
- package/dist/mcp/tools/agent-adapter-docs.d.ts.map +0 -1
- package/dist/mcp/tools/agent-coordination.d.ts +0 -9
- package/dist/mcp/tools/agent-coordination.d.ts.map +0 -1
- package/dist/mcp/tools/agent-runs.d.ts +0 -9
- package/dist/mcp/tools/agent-runs.d.ts.map +0 -1
- package/dist/mcp/tools/agent-workflow-demo.d.ts +0 -8
- package/dist/mcp/tools/agent-workflow-demo.d.ts.map +0 -1
- package/dist/mcp/tools/approval-gates.d.ts +0 -9
- package/dist/mcp/tools/approval-gates.d.ts.map +0 -1
- package/dist/mcp/tools/artifacts.d.ts +0 -9
- package/dist/mcp/tools/artifacts.d.ts.map +0 -1
- package/dist/mcp/tools/branch-work-plans.d.ts +0 -8
- package/dist/mcp/tools/branch-work-plans.d.ts.map +0 -1
- package/dist/mcp/tools/cli-docs.d.ts +0 -8
- package/dist/mcp/tools/cli-docs.d.ts.map +0 -1
- package/dist/mcp/tools/command-aliases.d.ts +0 -8
- package/dist/mcp/tools/command-aliases.d.ts.map +0 -1
- package/dist/mcp/tools/context-packs.d.ts +0 -9
- package/dist/mcp/tools/context-packs.d.ts.map +0 -1
- package/dist/mcp/tools/crypto.d.ts +0 -8
- package/dist/mcp/tools/crypto.d.ts.map +0 -1
- package/dist/mcp/tools/db-backup.d.ts +0 -8
- package/dist/mcp/tools/db-backup.d.ts.map +0 -1
- package/dist/mcp/tools/decision-records.d.ts +0 -9
- package/dist/mcp/tools/decision-records.d.ts.map +0 -1
- package/dist/mcp/tools/dependency-graph.d.ts +0 -9
- package/dist/mcp/tools/dependency-graph.d.ts.map +0 -1
- package/dist/mcp/tools/failure-triage.d.ts +0 -9
- package/dist/mcp/tools/failure-triage.d.ts.map +0 -1
- package/dist/mcp/tools/feature-manifest.d.ts +0 -8
- package/dist/mcp/tools/feature-manifest.d.ts.map +0 -1
- package/dist/mcp/tools/git-traceability.d.ts +0 -9
- package/dist/mcp/tools/git-traceability.d.ts.map +0 -1
- package/dist/mcp/tools/goal.d.ts +0 -9
- package/dist/mcp/tools/goal.d.ts.map +0 -1
- package/dist/mcp/tools/handoff-packets.d.ts +0 -9
- package/dist/mcp/tools/handoff-packets.d.ts.map +0 -1
- package/dist/mcp/tools/import-export-bridge.d.ts +0 -9
- package/dist/mcp/tools/import-export-bridge.d.ts.map +0 -1
- package/dist/mcp/tools/inbox-intake.d.ts +0 -9
- package/dist/mcp/tools/inbox-intake.d.ts.map +0 -1
- package/dist/mcp/tools/issue-importers.d.ts +0 -9
- package/dist/mcp/tools/issue-importers.d.ts.map +0 -1
- package/dist/mcp/tools/json-schemas.d.ts +0 -8
- package/dist/mcp/tools/json-schemas.d.ts.map +0 -1
- package/dist/mcp/tools/machine-topology.d.ts +0 -8
- package/dist/mcp/tools/machine-topology.d.ts.map +0 -1
- package/dist/mcp/tools/mention-resolver.d.ts +0 -9
- package/dist/mcp/tools/mention-resolver.d.ts.map +0 -1
- package/dist/mcp/tools/nl-intake.d.ts +0 -9
- package/dist/mcp/tools/nl-intake.d.ts.map +0 -1
- package/dist/mcp/tools/notification-reminders.d.ts +0 -9
- package/dist/mcp/tools/notification-reminders.d.ts.map +0 -1
- package/dist/mcp/tools/parity.d.ts +0 -8
- package/dist/mcp/tools/parity.d.ts.map +0 -1
- package/dist/mcp/tools/plan-execution.d.ts +0 -9
- package/dist/mcp/tools/plan-execution.d.ts.map +0 -1
- package/dist/mcp/tools/policy-packs.d.ts +0 -9
- package/dist/mcp/tools/policy-packs.d.ts.map +0 -1
- package/dist/mcp/tools/project-bootstrap.d.ts +0 -8
- package/dist/mcp/tools/project-bootstrap.d.ts.map +0 -1
- package/dist/mcp/tools/release-checks.d.ts +0 -8
- package/dist/mcp/tools/release-checks.d.ts.map +0 -1
- package/dist/mcp/tools/release-notes.d.ts +0 -9
- package/dist/mcp/tools/release-notes.d.ts.map +0 -1
- package/dist/mcp/tools/report-exports.d.ts +0 -9
- package/dist/mcp/tools/report-exports.d.ts.map +0 -1
- package/dist/mcp/tools/resource-subscriptions.d.ts +0 -8
- package/dist/mcp/tools/resource-subscriptions.d.ts.map +0 -1
- package/dist/mcp/tools/run-records.d.ts +0 -9
- package/dist/mcp/tools/run-records.d.ts.map +0 -1
- package/dist/mcp/tools/sandbox.d.ts +0 -8
- package/dist/mcp/tools/sandbox.d.ts.map +0 -1
- package/dist/mcp/tools/saved-views.d.ts +0 -9
- package/dist/mcp/tools/saved-views.d.ts.map +0 -1
- package/dist/mcp/tools/secret-redaction.d.ts +0 -8
- package/dist/mcp/tools/secret-redaction.d.ts.map +0 -1
- package/dist/mcp/tools/task-dedupe.d.ts +0 -9
- package/dist/mcp/tools/task-dedupe.d.ts.map +0 -1
- package/dist/mcp/tools/task-scheduling.d.ts +0 -9
- package/dist/mcp/tools/task-scheduling.d.ts.map +0 -1
- package/dist/mcp/tools/template-library.d.ts +0 -8
- package/dist/mcp/tools/template-library.d.ts.map +0 -1
- package/dist/mcp/tools/terminal-notifications.d.ts +0 -9
- package/dist/mcp/tools/terminal-notifications.d.ts.map +0 -1
- package/dist/mcp/tools/todos-md.d.ts +0 -8
- package/dist/mcp/tools/todos-md.d.ts.map +0 -1
- package/dist/mcp/tools/user-scaffolds.d.ts +0 -8
- package/dist/mcp/tools/user-scaffolds.d.ts.map +0 -1
- package/dist/mcp/tools/verification.d.ts +0 -9
- package/dist/mcp/tools/verification.d.ts.map +0 -1
- package/dist/mcp/tools/webhooks.d.ts +0 -8
- package/dist/mcp/tools/webhooks.d.ts.map +0 -1
- package/dist/mcp/tools/workspace-trust.d.ts +0 -8
- package/dist/mcp/tools/workspace-trust.d.ts.map +0 -1
- package/dist/test/no-network.d.ts +0 -7
- package/dist/test/no-network.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1223,6 +1223,15 @@ function ensureSchema(db) {
|
|
|
1223
1223
|
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1224
1224
|
tag TEXT NOT NULL, PRIMARY KEY (task_id, tag)
|
|
1225
1225
|
)`);
|
|
1226
|
+
ensureTable("tags", `
|
|
1227
|
+
CREATE TABLE tags (
|
|
1228
|
+
id TEXT PRIMARY KEY,
|
|
1229
|
+
name TEXT NOT NULL UNIQUE,
|
|
1230
|
+
color TEXT,
|
|
1231
|
+
description TEXT,
|
|
1232
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1233
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1234
|
+
)`);
|
|
1226
1235
|
ensureTable("task_dependencies", `
|
|
1227
1236
|
CREATE TABLE task_dependencies (
|
|
1228
1237
|
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
@@ -1436,6 +1445,36 @@ function ensureSchema(db) {
|
|
|
1436
1445
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_git_refs_task ON task_git_refs(task_id)");
|
|
1437
1446
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_git_refs_lookup ON task_git_refs(ref_type, name)");
|
|
1438
1447
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_git_refs_url ON task_git_refs(url)");
|
|
1448
|
+
ensureTable("task_commits", `
|
|
1449
|
+
CREATE TABLE task_commits (
|
|
1450
|
+
id TEXT PRIMARY KEY,
|
|
1451
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1452
|
+
sha TEXT NOT NULL,
|
|
1453
|
+
message TEXT,
|
|
1454
|
+
author TEXT,
|
|
1455
|
+
files_changed TEXT,
|
|
1456
|
+
committed_at TEXT,
|
|
1457
|
+
branch TEXT,
|
|
1458
|
+
pr_url TEXT,
|
|
1459
|
+
pr_number INTEGER,
|
|
1460
|
+
pr_state TEXT,
|
|
1461
|
+
ci_snapshot TEXT,
|
|
1462
|
+
release_tag TEXT,
|
|
1463
|
+
repo_path TEXT,
|
|
1464
|
+
traceability TEXT DEFAULT '{}',
|
|
1465
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1466
|
+
UNIQUE(task_id, sha)
|
|
1467
|
+
)`);
|
|
1468
|
+
ensureColumn("task_commits", "branch", "TEXT");
|
|
1469
|
+
ensureColumn("task_commits", "pr_url", "TEXT");
|
|
1470
|
+
ensureColumn("task_commits", "pr_number", "INTEGER");
|
|
1471
|
+
ensureColumn("task_commits", "pr_state", "TEXT");
|
|
1472
|
+
ensureColumn("task_commits", "ci_snapshot", "TEXT");
|
|
1473
|
+
ensureColumn("task_commits", "release_tag", "TEXT");
|
|
1474
|
+
ensureColumn("task_commits", "repo_path", "TEXT");
|
|
1475
|
+
ensureColumn("task_commits", "traceability", "TEXT DEFAULT '{}'");
|
|
1476
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_commits_task ON task_commits(task_id)");
|
|
1477
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_commits_sha ON task_commits(sha)");
|
|
1439
1478
|
ensureTable("task_verifications", `
|
|
1440
1479
|
CREATE TABLE task_verifications (
|
|
1441
1480
|
id TEXT PRIMARY KEY,
|
|
@@ -1620,6 +1659,9 @@ function ensureSchema(db) {
|
|
|
1620
1659
|
ensureColumn("tasks", "max_retries", "INTEGER DEFAULT 3");
|
|
1621
1660
|
ensureColumn("tasks", "retry_after", "TEXT");
|
|
1622
1661
|
ensureColumn("tasks", "sla_minutes", "INTEGER");
|
|
1662
|
+
ensureColumn("tasks", "scheduled_start_at", "TEXT");
|
|
1663
|
+
ensureColumn("tasks", "priority_score", "INTEGER");
|
|
1664
|
+
ensureColumn("tasks", "priority_reason", "TEXT");
|
|
1623
1665
|
ensureColumn("tasks", "archived_at", "TEXT");
|
|
1624
1666
|
ensureColumn("agents", "role", "TEXT DEFAULT 'agent'");
|
|
1625
1667
|
ensureColumn("agents", "permissions", `TEXT DEFAULT '["*"]'`);
|
|
@@ -1742,6 +1784,7 @@ function ensureSchema(db) {
|
|
|
1742
1784
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_lists_slug ON task_lists(slug)");
|
|
1743
1785
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_tags_tag ON task_tags(tag)");
|
|
1744
1786
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_tags_task ON task_tags(task_id)");
|
|
1787
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tags_name ON tags(name)");
|
|
1745
1788
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_project ON plans(project_id)");
|
|
1746
1789
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_status ON plans(status)");
|
|
1747
1790
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plans_task_list ON plans(task_list_id)");
|
|
@@ -1879,6 +1922,217 @@ function ensureSchema(db) {
|
|
|
1879
1922
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_cycles_dates ON cycles(start_date, end_date)");
|
|
1880
1923
|
ensureColumn("tasks", "cycle_id", "TEXT REFERENCES cycles(id) ON DELETE SET NULL");
|
|
1881
1924
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_cycle ON tasks(cycle_id) WHERE cycle_id IS NOT NULL");
|
|
1925
|
+
ensureTable("labels", `
|
|
1926
|
+
CREATE TABLE labels (
|
|
1927
|
+
id TEXT PRIMARY KEY,
|
|
1928
|
+
project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
1929
|
+
name TEXT NOT NULL,
|
|
1930
|
+
color TEXT,
|
|
1931
|
+
description TEXT,
|
|
1932
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1933
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1934
|
+
UNIQUE(project_id, name)
|
|
1935
|
+
)`);
|
|
1936
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_labels_project ON labels(project_id)");
|
|
1937
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_labels_name ON labels(name)");
|
|
1938
|
+
ensureTable("task_labels", `
|
|
1939
|
+
CREATE TABLE task_labels (
|
|
1940
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1941
|
+
label_id TEXT NOT NULL REFERENCES labels(id) ON DELETE CASCADE,
|
|
1942
|
+
PRIMARY KEY (task_id, label_id)
|
|
1943
|
+
)`);
|
|
1944
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_labels_task ON task_labels(task_id)");
|
|
1945
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_labels_label ON task_labels(label_id)");
|
|
1946
|
+
ensureTable("custom_field_definitions", `
|
|
1947
|
+
CREATE TABLE custom_field_definitions (
|
|
1948
|
+
id TEXT PRIMARY KEY,
|
|
1949
|
+
project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
|
|
1950
|
+
name TEXT NOT NULL,
|
|
1951
|
+
slug TEXT NOT NULL,
|
|
1952
|
+
field_type TEXT NOT NULL CHECK(field_type IN ('text', 'number', 'boolean', 'date', 'enum')),
|
|
1953
|
+
options TEXT DEFAULT '[]',
|
|
1954
|
+
required INTEGER NOT NULL DEFAULT 0,
|
|
1955
|
+
default_value TEXT,
|
|
1956
|
+
sort_order INTEGER NOT NULL DEFAULT 0,
|
|
1957
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1958
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1959
|
+
UNIQUE(project_id, slug)
|
|
1960
|
+
)`);
|
|
1961
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_custom_fields_project ON custom_field_definitions(project_id)");
|
|
1962
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_custom_fields_slug ON custom_field_definitions(slug)");
|
|
1963
|
+
ensureTable("task_custom_field_values", `
|
|
1964
|
+
CREATE TABLE task_custom_field_values (
|
|
1965
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
1966
|
+
field_id TEXT NOT NULL REFERENCES custom_field_definitions(id) ON DELETE CASCADE,
|
|
1967
|
+
value TEXT,
|
|
1968
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1969
|
+
PRIMARY KEY (task_id, field_id)
|
|
1970
|
+
)`);
|
|
1971
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_custom_values_task ON task_custom_field_values(task_id)");
|
|
1972
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_custom_values_field ON task_custom_field_values(field_id)");
|
|
1973
|
+
ensureTable("saved_views", `
|
|
1974
|
+
CREATE TABLE saved_views (
|
|
1975
|
+
id TEXT PRIMARY KEY,
|
|
1976
|
+
name TEXT NOT NULL UNIQUE,
|
|
1977
|
+
slug TEXT NOT NULL UNIQUE,
|
|
1978
|
+
entity_type TEXT NOT NULL DEFAULT 'task',
|
|
1979
|
+
filters TEXT NOT NULL DEFAULT '{}',
|
|
1980
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1981
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1982
|
+
)`);
|
|
1983
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_saved_views_slug ON saved_views(slug)");
|
|
1984
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_saved_views_entity ON saved_views(entity_type)");
|
|
1985
|
+
ensureTable("decision_records", `
|
|
1986
|
+
CREATE TABLE decision_records (
|
|
1987
|
+
id TEXT PRIMARY KEY,
|
|
1988
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
1989
|
+
task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
|
|
1990
|
+
plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
|
|
1991
|
+
agent_id TEXT,
|
|
1992
|
+
sequence_num INTEGER NOT NULL,
|
|
1993
|
+
short_ref TEXT NOT NULL UNIQUE,
|
|
1994
|
+
title TEXT NOT NULL,
|
|
1995
|
+
status TEXT NOT NULL DEFAULT 'proposed' CHECK(status IN ('proposed', 'accepted', 'deprecated', 'superseded', 'rejected')),
|
|
1996
|
+
context TEXT,
|
|
1997
|
+
decision TEXT NOT NULL,
|
|
1998
|
+
consequences TEXT,
|
|
1999
|
+
alternatives TEXT DEFAULT '[]',
|
|
2000
|
+
tags TEXT DEFAULT '[]',
|
|
2001
|
+
supersedes_id TEXT REFERENCES decision_records(id) ON DELETE SET NULL,
|
|
2002
|
+
superseded_by_id TEXT REFERENCES decision_records(id) ON DELETE SET NULL,
|
|
2003
|
+
metadata TEXT DEFAULT '{}',
|
|
2004
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2005
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2006
|
+
)`);
|
|
2007
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_decision_records_project ON decision_records(project_id)");
|
|
2008
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_decision_records_task ON decision_records(task_id)");
|
|
2009
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_decision_records_plan ON decision_records(plan_id)");
|
|
2010
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_decision_records_status ON decision_records(status)");
|
|
2011
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_decision_records_short_ref ON decision_records(short_ref)");
|
|
2012
|
+
ensureTable("knowledge_snapshots", `
|
|
2013
|
+
CREATE TABLE knowledge_snapshots (
|
|
2014
|
+
id TEXT PRIMARY KEY,
|
|
2015
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
2016
|
+
title TEXT NOT NULL,
|
|
2017
|
+
summary TEXT,
|
|
2018
|
+
content_hash TEXT NOT NULL,
|
|
2019
|
+
snapshot TEXT NOT NULL,
|
|
2020
|
+
decision_ids TEXT DEFAULT '[]',
|
|
2021
|
+
topics TEXT DEFAULT '[]',
|
|
2022
|
+
source TEXT NOT NULL DEFAULT 'auto' CHECK(source IN ('manual', 'auto', 'import')),
|
|
2023
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2024
|
+
)`);
|
|
2025
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_knowledge_snapshots_project ON knowledge_snapshots(project_id)");
|
|
2026
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_knowledge_snapshots_hash ON knowledge_snapshots(content_hash)");
|
|
2027
|
+
ensureTable("verification_records", `
|
|
2028
|
+
CREATE TABLE verification_records (
|
|
2029
|
+
id TEXT PRIMARY KEY,
|
|
2030
|
+
task_id TEXT REFERENCES tasks(id) ON DELETE CASCADE,
|
|
2031
|
+
provider_name TEXT NOT NULL,
|
|
2032
|
+
provider_type TEXT NOT NULL,
|
|
2033
|
+
status TEXT NOT NULL DEFAULT 'unknown' CHECK(status IN ('passed', 'failed', 'unknown')),
|
|
2034
|
+
summary TEXT,
|
|
2035
|
+
evidence TEXT DEFAULT '{}',
|
|
2036
|
+
artifact_id TEXT,
|
|
2037
|
+
started_at TEXT,
|
|
2038
|
+
completed_at TEXT,
|
|
2039
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2040
|
+
)`);
|
|
2041
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_verification_records_task ON verification_records(task_id)");
|
|
2042
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_verification_records_status ON verification_records(status)");
|
|
2043
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_verification_records_created ON verification_records(created_at)");
|
|
2044
|
+
ensureTable("task_leases", `
|
|
2045
|
+
CREATE TABLE task_leases (
|
|
2046
|
+
task_id TEXT PRIMARY KEY REFERENCES tasks(id) ON DELETE CASCADE,
|
|
2047
|
+
agent_id TEXT NOT NULL,
|
|
2048
|
+
acquired_at TEXT NOT NULL,
|
|
2049
|
+
expires_at TEXT NOT NULL,
|
|
2050
|
+
heartbeat_at TEXT,
|
|
2051
|
+
steal_count INTEGER NOT NULL DEFAULT 0,
|
|
2052
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2053
|
+
)`);
|
|
2054
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_leases_agent ON task_leases(agent_id)");
|
|
2055
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_task_leases_expires ON task_leases(expires_at)");
|
|
2056
|
+
ensureTable("reminder_preferences", `
|
|
2057
|
+
CREATE TABLE reminder_preferences (
|
|
2058
|
+
id TEXT PRIMARY KEY,
|
|
2059
|
+
due_soon_hours INTEGER NOT NULL DEFAULT 24,
|
|
2060
|
+
sla_warning_minutes INTEGER NOT NULL DEFAULT 30,
|
|
2061
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
2062
|
+
desktop_notify INTEGER NOT NULL DEFAULT 0,
|
|
2063
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2064
|
+
)`);
|
|
2065
|
+
ensureTable("notification_reminders", `
|
|
2066
|
+
CREATE TABLE notification_reminders (
|
|
2067
|
+
id TEXT PRIMARY KEY,
|
|
2068
|
+
task_id TEXT REFERENCES tasks(id) ON DELETE CASCADE,
|
|
2069
|
+
reminder_type TEXT NOT NULL CHECK(reminder_type IN ('due_soon', 'due_overdue', 'sla_warning', 'sla_breach', 'custom')),
|
|
2070
|
+
title TEXT NOT NULL,
|
|
2071
|
+
message TEXT,
|
|
2072
|
+
trigger_at TEXT NOT NULL,
|
|
2073
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN ('pending', 'fired', 'dismissed', 'snoozed')),
|
|
2074
|
+
snoozed_until TEXT,
|
|
2075
|
+
project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
|
|
2076
|
+
agent_id TEXT,
|
|
2077
|
+
priority TEXT NOT NULL DEFAULT 'medium',
|
|
2078
|
+
metadata TEXT DEFAULT '{}',
|
|
2079
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2080
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2081
|
+
fired_at TEXT,
|
|
2082
|
+
dismissed_at TEXT
|
|
2083
|
+
)`);
|
|
2084
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_notification_reminders_task ON notification_reminders(task_id)");
|
|
2085
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_notification_reminders_status ON notification_reminders(status)");
|
|
2086
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_notification_reminders_trigger ON notification_reminders(trigger_at)");
|
|
2087
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_notification_reminders_project ON notification_reminders(project_id)");
|
|
2088
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_notification_reminders_agent ON notification_reminders(agent_id)");
|
|
2089
|
+
ensureTable("run_records", `
|
|
2090
|
+
CREATE TABLE run_records (
|
|
2091
|
+
id TEXT PRIMARY KEY,
|
|
2092
|
+
agent_run_id TEXT,
|
|
2093
|
+
agent_id TEXT,
|
|
2094
|
+
objective TEXT,
|
|
2095
|
+
plan_id TEXT REFERENCES plans(id) ON DELETE SET NULL,
|
|
2096
|
+
claimed_task_ids TEXT DEFAULT '[]',
|
|
2097
|
+
commands TEXT DEFAULT '[]',
|
|
2098
|
+
stdout_summary TEXT,
|
|
2099
|
+
stderr_summary TEXT,
|
|
2100
|
+
files_touched TEXT DEFAULT '[]',
|
|
2101
|
+
verification_results TEXT DEFAULT '[]',
|
|
2102
|
+
artifact_ids TEXT DEFAULT '[]',
|
|
2103
|
+
status_transitions TEXT DEFAULT '[]',
|
|
2104
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK(status IN ('active', 'completed', 'failed', 'archived')),
|
|
2105
|
+
replay_bundle TEXT,
|
|
2106
|
+
metadata TEXT DEFAULT '{}',
|
|
2107
|
+
started_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2108
|
+
completed_at TEXT,
|
|
2109
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2110
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2111
|
+
)`);
|
|
2112
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_run_records_agent_run ON run_records(agent_run_id)");
|
|
2113
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_run_records_agent ON run_records(agent_id)");
|
|
2114
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_run_records_plan ON run_records(plan_id)");
|
|
2115
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_run_records_status ON run_records(status)");
|
|
2116
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_run_records_started ON run_records(started_at)");
|
|
2117
|
+
ensureTable("activity_log", `
|
|
2118
|
+
CREATE TABLE activity_log (
|
|
2119
|
+
id TEXT PRIMARY KEY,
|
|
2120
|
+
entity_type TEXT NOT NULL,
|
|
2121
|
+
entity_id TEXT NOT NULL,
|
|
2122
|
+
action TEXT NOT NULL,
|
|
2123
|
+
field TEXT,
|
|
2124
|
+
old_value TEXT,
|
|
2125
|
+
new_value TEXT,
|
|
2126
|
+
actor_id TEXT,
|
|
2127
|
+
session_id TEXT,
|
|
2128
|
+
machine_id TEXT,
|
|
2129
|
+
metadata TEXT DEFAULT '{}',
|
|
2130
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
2131
|
+
)`);
|
|
2132
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_activity_log_entity ON activity_log(entity_type, entity_id)");
|
|
2133
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_activity_log_actor ON activity_log(actor_id)");
|
|
2134
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_activity_log_action ON activity_log(action)");
|
|
2135
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_activity_log_created ON activity_log(created_at)");
|
|
1882
2136
|
ensureTable("api_keys", `
|
|
1883
2137
|
CREATE TABLE api_keys (
|
|
1884
2138
|
id TEXT PRIMARY KEY,
|
|
@@ -2315,11 +2569,17 @@ function ensureDir(filePath) {
|
|
|
2315
2569
|
}
|
|
2316
2570
|
}
|
|
2317
2571
|
function getDatabase(dbPath) {
|
|
2318
|
-
if (_db)
|
|
2319
|
-
return _db;
|
|
2320
2572
|
const path = dbPath || getDbPath();
|
|
2573
|
+
if (_db && _dbPath === path)
|
|
2574
|
+
return _db;
|
|
2575
|
+
if (_db && _dbPath !== path) {
|
|
2576
|
+
_db.close();
|
|
2577
|
+
_db = null;
|
|
2578
|
+
_dbPath = null;
|
|
2579
|
+
}
|
|
2321
2580
|
ensureDir(path);
|
|
2322
2581
|
_db = new Database(path);
|
|
2582
|
+
_dbPath = path;
|
|
2323
2583
|
_db.run("PRAGMA journal_mode = WAL");
|
|
2324
2584
|
_db.run("PRAGMA busy_timeout = 5000");
|
|
2325
2585
|
_db.run("PRAGMA foreign_keys = ON");
|
|
@@ -2332,10 +2592,12 @@ function closeDatabase() {
|
|
|
2332
2592
|
if (_db) {
|
|
2333
2593
|
_db.close();
|
|
2334
2594
|
_db = null;
|
|
2595
|
+
_dbPath = null;
|
|
2335
2596
|
}
|
|
2336
2597
|
}
|
|
2337
2598
|
function resetDatabase() {
|
|
2338
2599
|
_db = null;
|
|
2600
|
+
_dbPath = null;
|
|
2339
2601
|
}
|
|
2340
2602
|
function now() {
|
|
2341
2603
|
return new Date().toISOString();
|
|
@@ -2396,7 +2658,7 @@ function resolvePartialId(db, table, partialId) {
|
|
|
2396
2658
|
}
|
|
2397
2659
|
return null;
|
|
2398
2660
|
}
|
|
2399
|
-
var LOCK_EXPIRY_MINUTES = 30, _db = null, ALLOWED_TABLES;
|
|
2661
|
+
var LOCK_EXPIRY_MINUTES = 30, _db = null, _dbPath = null, ALLOWED_TABLES;
|
|
2400
2662
|
var init_database = __esm(() => {
|
|
2401
2663
|
init_schema();
|
|
2402
2664
|
init_machines();
|
|
@@ -2888,9 +3150,10 @@ function redactExportRecord(record) {
|
|
|
2888
3150
|
function getDefaultSecretPatterns() {
|
|
2889
3151
|
return DEFAULT_PATTERNS.map((p) => ({ name: p.name, source: p.pattern.source }));
|
|
2890
3152
|
}
|
|
2891
|
-
var SECRET_REDACTION_SCHEMA
|
|
3153
|
+
var SECRET_REDACTION_SCHEMA, REDACTION_PLACEHOLDER = "[REDACTED]", DEFAULT_PATTERNS, DEFAULT_ALLOWLIST, customRedactors;
|
|
2892
3154
|
var init_secret_redaction = __esm(() => {
|
|
2893
3155
|
init_redaction();
|
|
3156
|
+
SECRET_REDACTION_SCHEMA = ["todos", "secret_redaction", "v1"].join(".");
|
|
2894
3157
|
DEFAULT_PATTERNS = [
|
|
2895
3158
|
{ name: "openai_sk", pattern: /\bsk-[a-zA-Z0-9]{10,}\b/g },
|
|
2896
3159
|
{ name: "github_pat", pattern: /\bghp_[a-zA-Z0-9]{20,}\b/g },
|
|
@@ -9738,7 +10001,7 @@ function storeArtifactFile(input) {
|
|
|
9738
10001
|
}
|
|
9739
10002
|
return { sourcePath, localPath, contentHash, mimeType, sizeBytes: buffer.length };
|
|
9740
10003
|
}
|
|
9741
|
-
function deleteStoredArtifactFile(localPath, storageMode,
|
|
10004
|
+
function deleteStoredArtifactFile(localPath, storageMode, _dbPath2) {
|
|
9742
10005
|
if (storageMode === "reference")
|
|
9743
10006
|
return false;
|
|
9744
10007
|
if (!localPath || !existsSync7(localPath))
|
|
@@ -9993,11 +10256,19 @@ function getTaskVerifications(taskId, db) {
|
|
|
9993
10256
|
}
|
|
9994
10257
|
function getTaskTraceability(taskId, db) {
|
|
9995
10258
|
const d = db || getDatabase();
|
|
10259
|
+
const commits = getTaskCommits(taskId, d);
|
|
10260
|
+
const gitRefs = getTaskGitRefs(taskId, d);
|
|
9996
10261
|
return {
|
|
9997
10262
|
task_id: taskId,
|
|
9998
|
-
commits
|
|
9999
|
-
git_refs:
|
|
10000
|
-
verifications: getTaskVerifications(taskId, d)
|
|
10263
|
+
commits,
|
|
10264
|
+
git_refs: gitRefs,
|
|
10265
|
+
verifications: getTaskVerifications(taskId, d),
|
|
10266
|
+
branches: Array.from(new Set([
|
|
10267
|
+
...commits.map((commit) => commit.branch).filter((branch) => Boolean(branch)),
|
|
10268
|
+
...gitRefs.filter((ref) => ref.ref_type === "branch").map((ref) => ref.name)
|
|
10269
|
+
])).sort(),
|
|
10270
|
+
release_tags: Array.from(new Set(commits.map((commit) => commit.release_tag).filter((tag) => Boolean(tag)))).sort(),
|
|
10271
|
+
pull_requests: commits.filter((commit) => commit.pr_url).map((commit) => ({ url: commit.pr_url, state: commit.pr_state, number: commit.pr_number }))
|
|
10001
10272
|
};
|
|
10002
10273
|
}
|
|
10003
10274
|
|
|
@@ -17057,6 +17328,16 @@ function parseClock(value) {
|
|
|
17057
17328
|
throw new Error("quiet hours must use HH:MM-HH:MM");
|
|
17058
17329
|
return Number(match[1]) * 60 + Number(match[2]);
|
|
17059
17330
|
}
|
|
17331
|
+
function parseQuietHours(value, timezone = "local") {
|
|
17332
|
+
if (!value)
|
|
17333
|
+
return;
|
|
17334
|
+
const [start, end] = value.split("-", 2).map((part) => part.trim());
|
|
17335
|
+
if (!start || !end)
|
|
17336
|
+
throw new Error("quiet hours must use HH:MM-HH:MM");
|
|
17337
|
+
parseClock(start);
|
|
17338
|
+
parseClock(end);
|
|
17339
|
+
return { start, end, timezone };
|
|
17340
|
+
}
|
|
17060
17341
|
function isQuietTime(quiet, timestamp2) {
|
|
17061
17342
|
if (!quiet)
|
|
17062
17343
|
return false;
|
|
@@ -18979,6 +19260,168 @@ var TODOS_REGISTRY = createTodosRegistry({
|
|
|
18979
19260
|
generatedAt: "1970-01-01T00:00:00.000Z"
|
|
18980
19261
|
});
|
|
18981
19262
|
|
|
19263
|
+
// src/storage/config.ts
|
|
19264
|
+
var TODOS_STORAGE_TABLES = [
|
|
19265
|
+
"todos_sync_records",
|
|
19266
|
+
"todos_sync_cursors"
|
|
19267
|
+
];
|
|
19268
|
+
var STORAGE_TABLES = TODOS_STORAGE_TABLES;
|
|
19269
|
+
var TODOS_STORAGE_ENV = {
|
|
19270
|
+
mode: "HASNA_TODOS_STORAGE_MODE",
|
|
19271
|
+
databaseUrl: "HASNA_TODOS_DATABASE_URL",
|
|
19272
|
+
databaseSsl: "HASNA_TODOS_DATABASE_SSL",
|
|
19273
|
+
databaseSchema: "HASNA_TODOS_DATABASE_SCHEMA",
|
|
19274
|
+
s3Bucket: "HASNA_TODOS_S3_BUCKET",
|
|
19275
|
+
s3Prefix: "HASNA_TODOS_S3_PREFIX",
|
|
19276
|
+
awsRegion: "HASNA_TODOS_AWS_REGION",
|
|
19277
|
+
s3Endpoint: "HASNA_TODOS_S3_ENDPOINT",
|
|
19278
|
+
s3ForcePathStyle: "HASNA_TODOS_S3_FORCE_PATH_STYLE",
|
|
19279
|
+
s3AccessKeyId: "HASNA_TODOS_S3_ACCESS_KEY_ID",
|
|
19280
|
+
s3SecretAccessKey: "HASNA_TODOS_S3_SECRET_ACCESS_KEY",
|
|
19281
|
+
s3SessionToken: "HASNA_TODOS_S3_SESSION_TOKEN",
|
|
19282
|
+
syncBatchSize: "HASNA_TODOS_SYNC_BATCH_SIZE",
|
|
19283
|
+
syncDryRun: "HASNA_TODOS_SYNC_DRY_RUN"
|
|
19284
|
+
};
|
|
19285
|
+
var TODOS_STORAGE_FALLBACK_ENV = {
|
|
19286
|
+
mode: "TODOS_STORAGE_MODE",
|
|
19287
|
+
databaseUrl: "TODOS_DATABASE_URL",
|
|
19288
|
+
databaseSsl: "TODOS_DATABASE_SSL",
|
|
19289
|
+
databaseSchema: "TODOS_DATABASE_SCHEMA",
|
|
19290
|
+
s3Bucket: "TODOS_S3_BUCKET",
|
|
19291
|
+
s3Prefix: "TODOS_S3_PREFIX",
|
|
19292
|
+
awsRegion: "TODOS_AWS_REGION",
|
|
19293
|
+
s3Endpoint: "TODOS_S3_ENDPOINT",
|
|
19294
|
+
s3ForcePathStyle: "TODOS_S3_FORCE_PATH_STYLE",
|
|
19295
|
+
s3AccessKeyId: "TODOS_S3_ACCESS_KEY_ID",
|
|
19296
|
+
s3SecretAccessKey: "TODOS_S3_SECRET_ACCESS_KEY",
|
|
19297
|
+
s3SessionToken: "TODOS_S3_SESSION_TOKEN",
|
|
19298
|
+
syncBatchSize: "TODOS_SYNC_BATCH_SIZE",
|
|
19299
|
+
syncDryRun: "TODOS_SYNC_DRY_RUN"
|
|
19300
|
+
};
|
|
19301
|
+
var CANONICAL_TODOS_RDS_CLUSTER = "hasna-xyz-infra-apps-prod-postgres";
|
|
19302
|
+
var CANONICAL_TODOS_RDS_DATABASE = "todos";
|
|
19303
|
+
var CANONICAL_TODOS_RDS_RUNTIME_PATH = "hasna/xyz/opensource/todos/prod/rds";
|
|
19304
|
+
function getCanonicalTodosRdsConfig() {
|
|
19305
|
+
return {
|
|
19306
|
+
cluster: CANONICAL_TODOS_RDS_CLUSTER,
|
|
19307
|
+
database: CANONICAL_TODOS_RDS_DATABASE,
|
|
19308
|
+
runtimeSecretPath: CANONICAL_TODOS_RDS_RUNTIME_PATH,
|
|
19309
|
+
primaryEnv: TODOS_STORAGE_ENV.databaseUrl,
|
|
19310
|
+
fallbackEnv: TODOS_STORAGE_FALLBACK_ENV.databaseUrl
|
|
19311
|
+
};
|
|
19312
|
+
}
|
|
19313
|
+
function loadTodosStorageConfig(env = process.env) {
|
|
19314
|
+
const mode = getTodosStorageMode(env);
|
|
19315
|
+
const databaseUrl = getTodosStorageDatabaseUrl(env);
|
|
19316
|
+
const bucket = readStorageEnv(env, "s3Bucket").value;
|
|
19317
|
+
const prefix = readStorageEnv(env, "s3Prefix").value ?? "todos/";
|
|
19318
|
+
const region = readStorageEnv(env, "awsRegion").value;
|
|
19319
|
+
const endpoint = readStorageEnv(env, "s3Endpoint").value;
|
|
19320
|
+
const schema = readStorageEnv(env, "databaseSchema").value;
|
|
19321
|
+
return {
|
|
19322
|
+
service: "todos",
|
|
19323
|
+
mode,
|
|
19324
|
+
...databaseUrl ? {
|
|
19325
|
+
database: {
|
|
19326
|
+
provider: "postgres",
|
|
19327
|
+
url: databaseUrl,
|
|
19328
|
+
ssl: parseBoolean(readStorageEnv(env, "databaseSsl").value, true),
|
|
19329
|
+
...schema ? { schema } : {}
|
|
19330
|
+
}
|
|
19331
|
+
} : {},
|
|
19332
|
+
...bucket ? {
|
|
19333
|
+
objectStorage: {
|
|
19334
|
+
provider: "s3",
|
|
19335
|
+
bucket,
|
|
19336
|
+
prefix,
|
|
19337
|
+
...region ? { region } : {},
|
|
19338
|
+
...endpoint ? { endpoint } : {},
|
|
19339
|
+
forcePathStyle: parseBoolean(readStorageEnv(env, "s3ForcePathStyle").value, false)
|
|
19340
|
+
}
|
|
19341
|
+
} : {},
|
|
19342
|
+
sync: {
|
|
19343
|
+
batchSize: parsePositiveInteger(readStorageEnv(env, "syncBatchSize").value, 500),
|
|
19344
|
+
dryRun: parseBoolean(readStorageEnv(env, "syncDryRun").value, false)
|
|
19345
|
+
}
|
|
19346
|
+
};
|
|
19347
|
+
}
|
|
19348
|
+
function loadStorageConfig(env = process.env) {
|
|
19349
|
+
return loadTodosStorageConfig(env);
|
|
19350
|
+
}
|
|
19351
|
+
function isTodosRemoteStorageEnabled(config) {
|
|
19352
|
+
return config.mode === "remote" || config.mode === "hybrid";
|
|
19353
|
+
}
|
|
19354
|
+
function assertTodosRemoteStorageConfig(config) {
|
|
19355
|
+
if (!isTodosRemoteStorageEnabled(config))
|
|
19356
|
+
return;
|
|
19357
|
+
if (!config.database?.url) {
|
|
19358
|
+
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${TODOS_STORAGE_ENV.mode}=${config.mode}`);
|
|
19359
|
+
}
|
|
19360
|
+
}
|
|
19361
|
+
function parseStorageMode(value) {
|
|
19362
|
+
const normalized = clean(value)?.toLowerCase();
|
|
19363
|
+
if (!normalized)
|
|
19364
|
+
return "local";
|
|
19365
|
+
if (normalized === "local" || normalized === "remote" || normalized === "hybrid")
|
|
19366
|
+
return normalized;
|
|
19367
|
+
throw new Error(`${TODOS_STORAGE_ENV.mode} must be local, remote, or hybrid`);
|
|
19368
|
+
}
|
|
19369
|
+
function getTodosStorageMode(env = process.env) {
|
|
19370
|
+
return parseStorageMode(readStorageEnv(env, "mode").value);
|
|
19371
|
+
}
|
|
19372
|
+
function getStorageMode(env = process.env) {
|
|
19373
|
+
return getTodosStorageMode(env);
|
|
19374
|
+
}
|
|
19375
|
+
function getTodosStorageDatabaseEnv(env = process.env) {
|
|
19376
|
+
return readStorageEnv(env, "databaseUrl").name;
|
|
19377
|
+
}
|
|
19378
|
+
function getStorageDatabaseEnv(env = process.env) {
|
|
19379
|
+
return getTodosStorageDatabaseEnv(env);
|
|
19380
|
+
}
|
|
19381
|
+
function getTodosStorageDatabaseUrl(env = process.env) {
|
|
19382
|
+
return readStorageEnv(env, "databaseUrl").value;
|
|
19383
|
+
}
|
|
19384
|
+
function getStorageDatabaseUrl(env = process.env) {
|
|
19385
|
+
return getTodosStorageDatabaseUrl(env);
|
|
19386
|
+
}
|
|
19387
|
+
function getTodosStorageEnvName(env, key) {
|
|
19388
|
+
return readStorageEnv(env, key).name;
|
|
19389
|
+
}
|
|
19390
|
+
function readStorageEnv(env, key) {
|
|
19391
|
+
const primaryName = TODOS_STORAGE_ENV[key];
|
|
19392
|
+
const primaryValue = clean(env[primaryName]);
|
|
19393
|
+
if (primaryValue !== undefined)
|
|
19394
|
+
return { name: primaryName, value: primaryValue };
|
|
19395
|
+
const fallbackName = TODOS_STORAGE_FALLBACK_ENV[key];
|
|
19396
|
+
const fallbackValue = clean(env[fallbackName]);
|
|
19397
|
+
if (fallbackValue !== undefined)
|
|
19398
|
+
return { name: fallbackName, value: fallbackValue };
|
|
19399
|
+
return { name: primaryName };
|
|
19400
|
+
}
|
|
19401
|
+
function clean(value) {
|
|
19402
|
+
const trimmed = value?.trim();
|
|
19403
|
+
return trimmed ? trimmed : undefined;
|
|
19404
|
+
}
|
|
19405
|
+
function parseBoolean(value, fallback) {
|
|
19406
|
+
const normalized = clean(value)?.toLowerCase();
|
|
19407
|
+
if (!normalized)
|
|
19408
|
+
return fallback;
|
|
19409
|
+
if (["1", "true", "yes", "on"].includes(normalized))
|
|
19410
|
+
return true;
|
|
19411
|
+
if (["0", "false", "no", "off"].includes(normalized))
|
|
19412
|
+
return false;
|
|
19413
|
+
throw new Error(`Expected boolean env value, got ${value}`);
|
|
19414
|
+
}
|
|
19415
|
+
function parsePositiveInteger(value, fallback) {
|
|
19416
|
+
const normalized = clean(value);
|
|
19417
|
+
if (!normalized)
|
|
19418
|
+
return fallback;
|
|
19419
|
+
const parsed = Number.parseInt(normalized, 10);
|
|
19420
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
19421
|
+
throw new Error(`${TODOS_STORAGE_ENV.syncBatchSize} must be a positive integer`);
|
|
19422
|
+
}
|
|
19423
|
+
return parsed;
|
|
19424
|
+
}
|
|
18982
19425
|
// src/db/agents.ts
|
|
18983
19426
|
init_database();
|
|
18984
19427
|
|
|
@@ -19693,6 +20136,260 @@ function ensureTaskList(name, slug, projectId, db) {
|
|
|
19693
20136
|
|
|
19694
20137
|
// src/storage/local-sqlite.ts
|
|
19695
20138
|
init_database();
|
|
20139
|
+
|
|
20140
|
+
// src/storage/sqlite-snapshot.ts
|
|
20141
|
+
init_database();
|
|
20142
|
+
var PROJECT_COLUMNS = [
|
|
20143
|
+
"id",
|
|
20144
|
+
"name",
|
|
20145
|
+
"path",
|
|
20146
|
+
"description",
|
|
20147
|
+
"task_list_id",
|
|
20148
|
+
"task_prefix",
|
|
20149
|
+
"task_counter",
|
|
20150
|
+
"created_at",
|
|
20151
|
+
"updated_at",
|
|
20152
|
+
"machine_id",
|
|
20153
|
+
"synced_at"
|
|
20154
|
+
];
|
|
20155
|
+
var TASK_LIST_COLUMNS = [
|
|
20156
|
+
"id",
|
|
20157
|
+
"project_id",
|
|
20158
|
+
"slug",
|
|
20159
|
+
"name",
|
|
20160
|
+
"description",
|
|
20161
|
+
"metadata",
|
|
20162
|
+
"created_at",
|
|
20163
|
+
"updated_at",
|
|
20164
|
+
"machine_id",
|
|
20165
|
+
"synced_at"
|
|
20166
|
+
];
|
|
20167
|
+
var PLAN_COLUMNS = [
|
|
20168
|
+
"id",
|
|
20169
|
+
"project_id",
|
|
20170
|
+
"task_list_id",
|
|
20171
|
+
"agent_id",
|
|
20172
|
+
"name",
|
|
20173
|
+
"description",
|
|
20174
|
+
"status",
|
|
20175
|
+
"created_at",
|
|
20176
|
+
"updated_at",
|
|
20177
|
+
"machine_id",
|
|
20178
|
+
"synced_at"
|
|
20179
|
+
];
|
|
20180
|
+
var AGENT_COLUMNS = [
|
|
20181
|
+
"id",
|
|
20182
|
+
"name",
|
|
20183
|
+
"description",
|
|
20184
|
+
"role",
|
|
20185
|
+
"title",
|
|
20186
|
+
"level",
|
|
20187
|
+
"permissions",
|
|
20188
|
+
"capabilities",
|
|
20189
|
+
"reports_to",
|
|
20190
|
+
"org_id",
|
|
20191
|
+
"metadata",
|
|
20192
|
+
"status",
|
|
20193
|
+
"created_at",
|
|
20194
|
+
"last_seen_at",
|
|
20195
|
+
"session_id",
|
|
20196
|
+
"working_dir",
|
|
20197
|
+
"active_project_id",
|
|
20198
|
+
"machine_id",
|
|
20199
|
+
"synced_at"
|
|
20200
|
+
];
|
|
20201
|
+
var TEMPLATE_COLUMNS = [
|
|
20202
|
+
"id",
|
|
20203
|
+
"name",
|
|
20204
|
+
"title_pattern",
|
|
20205
|
+
"description",
|
|
20206
|
+
"priority",
|
|
20207
|
+
"tags",
|
|
20208
|
+
"variables",
|
|
20209
|
+
"project_id",
|
|
20210
|
+
"plan_id",
|
|
20211
|
+
"metadata",
|
|
20212
|
+
"version",
|
|
20213
|
+
"created_at",
|
|
20214
|
+
"machine_id",
|
|
20215
|
+
"synced_at"
|
|
20216
|
+
];
|
|
20217
|
+
var TASK_COLUMNS = [
|
|
20218
|
+
"id",
|
|
20219
|
+
"short_id",
|
|
20220
|
+
"project_id",
|
|
20221
|
+
"parent_id",
|
|
20222
|
+
"plan_id",
|
|
20223
|
+
"task_list_id",
|
|
20224
|
+
"title",
|
|
20225
|
+
"description",
|
|
20226
|
+
"status",
|
|
20227
|
+
"priority",
|
|
20228
|
+
"agent_id",
|
|
20229
|
+
"assigned_to",
|
|
20230
|
+
"session_id",
|
|
20231
|
+
"working_dir",
|
|
20232
|
+
"tags",
|
|
20233
|
+
"metadata",
|
|
20234
|
+
"version",
|
|
20235
|
+
"locked_by",
|
|
20236
|
+
"locked_at",
|
|
20237
|
+
"created_at",
|
|
20238
|
+
"updated_at",
|
|
20239
|
+
"started_at",
|
|
20240
|
+
"completed_at",
|
|
20241
|
+
"due_at",
|
|
20242
|
+
"estimated_minutes",
|
|
20243
|
+
"actual_minutes",
|
|
20244
|
+
"requires_approval",
|
|
20245
|
+
"approved_by",
|
|
20246
|
+
"approved_at",
|
|
20247
|
+
"recurrence_rule",
|
|
20248
|
+
"recurrence_parent_id",
|
|
20249
|
+
"spawns_template_id",
|
|
20250
|
+
"confidence",
|
|
20251
|
+
"reason",
|
|
20252
|
+
"spawned_from_session",
|
|
20253
|
+
"assigned_by",
|
|
20254
|
+
"assigned_from_project",
|
|
20255
|
+
"task_type",
|
|
20256
|
+
"cost_tokens",
|
|
20257
|
+
"cost_usd",
|
|
20258
|
+
"delegated_from",
|
|
20259
|
+
"delegation_depth",
|
|
20260
|
+
"retry_count",
|
|
20261
|
+
"max_retries",
|
|
20262
|
+
"retry_after",
|
|
20263
|
+
"sla_minutes",
|
|
20264
|
+
"runner_id",
|
|
20265
|
+
"runner_started_at",
|
|
20266
|
+
"runner_completed_at",
|
|
20267
|
+
"current_step",
|
|
20268
|
+
"total_steps",
|
|
20269
|
+
"cycle_id",
|
|
20270
|
+
"machine_id",
|
|
20271
|
+
"synced_at",
|
|
20272
|
+
"archived_at"
|
|
20273
|
+
];
|
|
20274
|
+
var AUDIT_COLUMNS = [
|
|
20275
|
+
"id",
|
|
20276
|
+
"task_id",
|
|
20277
|
+
"action",
|
|
20278
|
+
"field",
|
|
20279
|
+
"old_value",
|
|
20280
|
+
"new_value",
|
|
20281
|
+
"agent_id",
|
|
20282
|
+
"created_at",
|
|
20283
|
+
"machine_id"
|
|
20284
|
+
];
|
|
20285
|
+
var JSON_COLUMNS = new Set(["tags", "metadata", "permissions", "capabilities", "variables"]);
|
|
20286
|
+
var BOOLEAN_COLUMNS = new Set(["requires_approval"]);
|
|
20287
|
+
function exportSqliteTodosStorageSnapshot(db) {
|
|
20288
|
+
const d = db ?? getDatabase();
|
|
20289
|
+
return {
|
|
20290
|
+
exportedAt: new Date().toISOString(),
|
|
20291
|
+
source: "sqlite",
|
|
20292
|
+
tasks: listTasks({ include_archived: true }, d),
|
|
20293
|
+
projects: listProjects(d),
|
|
20294
|
+
plans: listPlans(undefined, d),
|
|
20295
|
+
agents: listAgents({ include_archived: true }, d),
|
|
20296
|
+
taskLists: listTaskLists(undefined, d),
|
|
20297
|
+
templates: listTemplates(d),
|
|
20298
|
+
auditHistory: getRecentActivity(Number.MAX_SAFE_INTEGER, d)
|
|
20299
|
+
};
|
|
20300
|
+
}
|
|
20301
|
+
function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
20302
|
+
const d = db ?? getDatabase();
|
|
20303
|
+
const result = {
|
|
20304
|
+
inserted: 0,
|
|
20305
|
+
updated: 0,
|
|
20306
|
+
skipped: 0,
|
|
20307
|
+
errors: []
|
|
20308
|
+
};
|
|
20309
|
+
const applyRows = (table, columns, rows, updateClockColumn, afterUpsert) => {
|
|
20310
|
+
for (const row of rows) {
|
|
20311
|
+
try {
|
|
20312
|
+
const record = asRecord2(row);
|
|
20313
|
+
const state = upsertById(d, table, columns, record, updateClockColumn);
|
|
20314
|
+
if (state === "inserted")
|
|
20315
|
+
result.inserted += 1;
|
|
20316
|
+
else if (state === "updated")
|
|
20317
|
+
result.updated += 1;
|
|
20318
|
+
else
|
|
20319
|
+
result.skipped += 1;
|
|
20320
|
+
afterUpsert?.(record, state !== "skipped");
|
|
20321
|
+
} catch (error) {
|
|
20322
|
+
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
20323
|
+
}
|
|
20324
|
+
}
|
|
20325
|
+
};
|
|
20326
|
+
applyRows("projects", PROJECT_COLUMNS, snapshot.projects, "updated_at");
|
|
20327
|
+
applyRows("agents", AGENT_COLUMNS, snapshot.agents, "last_seen_at");
|
|
20328
|
+
applyRows("task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
|
|
20329
|
+
applyRows("plans", PLAN_COLUMNS, snapshot.plans, "updated_at");
|
|
20330
|
+
applyRows("task_templates", TEMPLATE_COLUMNS, snapshot.templates);
|
|
20331
|
+
applyRows("tasks", TASK_COLUMNS, sortedTasks2(snapshot.tasks), "updated_at", (row, changed) => {
|
|
20332
|
+
if (changed && Array.isArray(row["tags"]) && typeof row["id"] === "string") {
|
|
20333
|
+
replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
|
|
20334
|
+
}
|
|
20335
|
+
});
|
|
20336
|
+
applyRows("task_history", AUDIT_COLUMNS, snapshot.auditHistory);
|
|
20337
|
+
return result;
|
|
20338
|
+
}
|
|
20339
|
+
function upsertById(db, table, columns, row, updateClockColumn) {
|
|
20340
|
+
const id = row["id"];
|
|
20341
|
+
if (typeof id !== "string" || !id)
|
|
20342
|
+
throw new Error(`${table} row is missing id`);
|
|
20343
|
+
const presentColumns = columns.filter((column) => (column in row));
|
|
20344
|
+
if (!presentColumns.includes("id"))
|
|
20345
|
+
presentColumns.unshift("id");
|
|
20346
|
+
const existing = existsById2(db, table, id);
|
|
20347
|
+
const placeholders = presentColumns.map(() => "?").join(", ");
|
|
20348
|
+
const values = presentColumns.map((column) => valueForColumn(column, row[column]));
|
|
20349
|
+
const updateColumns = presentColumns.filter((column) => column !== "id");
|
|
20350
|
+
const updateSet = updateColumns.map((column) => `${column} = excluded.${column}`).join(", ");
|
|
20351
|
+
const clockGuard = updateClockColumn && presentColumns.includes(updateClockColumn) ? ` WHERE ${table}.${updateClockColumn} IS NULL OR ${table}.${updateClockColumn} <= excluded.${updateClockColumn}` : "";
|
|
20352
|
+
const sql = updateSet ? `INSERT INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders})
|
|
20353
|
+
ON CONFLICT(id) DO UPDATE SET ${updateSet}${clockGuard}` : `INSERT OR IGNORE INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders})`;
|
|
20354
|
+
const changes = db.run(sql, values).changes;
|
|
20355
|
+
if (changes === 0)
|
|
20356
|
+
return "skipped";
|
|
20357
|
+
return existing ? "updated" : "inserted";
|
|
20358
|
+
}
|
|
20359
|
+
function existsById2(db, table, id) {
|
|
20360
|
+
return Boolean(db.query(`SELECT id FROM ${table} WHERE id = ?`).get(id));
|
|
20361
|
+
}
|
|
20362
|
+
function valueForColumn(column, value) {
|
|
20363
|
+
if (BOOLEAN_COLUMNS.has(column))
|
|
20364
|
+
return value ? 1 : 0;
|
|
20365
|
+
if (JSON_COLUMNS.has(column))
|
|
20366
|
+
return JSON.stringify(value ?? (column === "tags" || column === "permissions" || column === "capabilities" || column === "variables" ? [] : {}));
|
|
20367
|
+
return value === undefined ? null : value;
|
|
20368
|
+
}
|
|
20369
|
+
function asRecord2(value) {
|
|
20370
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
20371
|
+
throw new Error("snapshot rows must be objects");
|
|
20372
|
+
}
|
|
20373
|
+
return value;
|
|
20374
|
+
}
|
|
20375
|
+
function sortedTasks2(tasks) {
|
|
20376
|
+
const byId = new Map(tasks.map((task2) => [task2.id, task2]));
|
|
20377
|
+
const seen = new Set;
|
|
20378
|
+
const result = [];
|
|
20379
|
+
const visit = (task2) => {
|
|
20380
|
+
if (seen.has(task2.id))
|
|
20381
|
+
return;
|
|
20382
|
+
if (task2.parent_id && byId.has(task2.parent_id))
|
|
20383
|
+
visit(byId.get(task2.parent_id));
|
|
20384
|
+
seen.add(task2.id);
|
|
20385
|
+
result.push(task2);
|
|
20386
|
+
};
|
|
20387
|
+
for (const task2 of tasks)
|
|
20388
|
+
visit(task2);
|
|
20389
|
+
return result;
|
|
20390
|
+
}
|
|
20391
|
+
|
|
20392
|
+
// src/storage/local-sqlite.ts
|
|
19696
20393
|
function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
19697
20394
|
const database = () => options.db ?? getDatabase();
|
|
19698
20395
|
let adapter;
|
|
@@ -19765,7 +20462,9 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
19765
20462
|
getRecentActivity: (limit) => getRecentActivity(limit, database())
|
|
19766
20463
|
},
|
|
19767
20464
|
sync: {
|
|
19768
|
-
getTasksChangedSince: (since, filters) => getTasksChangedSince(since, filters, database())
|
|
20465
|
+
getTasksChangedSince: (since, filters) => getTasksChangedSince(since, filters, database()),
|
|
20466
|
+
exportSnapshot: () => exportSqliteTodosStorageSnapshot(database()),
|
|
20467
|
+
importSnapshot: (snapshot) => importSqliteTodosStorageSnapshot(snapshot, database())
|
|
19769
20468
|
},
|
|
19770
20469
|
transaction: (fn) => {
|
|
19771
20470
|
const tx = database().transaction(() => fn(adapter));
|
|
@@ -19774,6 +20473,1326 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
19774
20473
|
};
|
|
19775
20474
|
return adapter;
|
|
19776
20475
|
}
|
|
20476
|
+
|
|
20477
|
+
// src/storage/postgres-sync.ts
|
|
20478
|
+
var DEFAULT_TODOS_POSTGRES_SYNC_TABLE = "todos_sync_records";
|
|
20479
|
+
var DEFAULT_TODOS_POSTGRES_CURSOR_TABLE = "todos_sync_cursors";
|
|
20480
|
+
function postgresTodosSyncSchemaSql(tableName = DEFAULT_TODOS_POSTGRES_SYNC_TABLE, cursorTableName = DEFAULT_TODOS_POSTGRES_CURSOR_TABLE) {
|
|
20481
|
+
assertSafeIdentifier(tableName);
|
|
20482
|
+
assertSafeIdentifier(cursorTableName);
|
|
20483
|
+
return [
|
|
20484
|
+
`CREATE TABLE IF NOT EXISTS ${tableName} (
|
|
20485
|
+
service text NOT NULL,
|
|
20486
|
+
object_type text NOT NULL,
|
|
20487
|
+
object_id text NOT NULL,
|
|
20488
|
+
payload jsonb NOT NULL,
|
|
20489
|
+
updated_at timestamptz NOT NULL,
|
|
20490
|
+
deleted_at timestamptz,
|
|
20491
|
+
source_machine_id text,
|
|
20492
|
+
version integer,
|
|
20493
|
+
PRIMARY KEY (service, object_type, object_id)
|
|
20494
|
+
)`,
|
|
20495
|
+
`CREATE INDEX IF NOT EXISTS ${tableName}_updated_idx ON ${tableName} (service, updated_at)`,
|
|
20496
|
+
`CREATE TABLE IF NOT EXISTS ${cursorTableName} (
|
|
20497
|
+
service text NOT NULL,
|
|
20498
|
+
cursor_name text NOT NULL,
|
|
20499
|
+
value text NOT NULL,
|
|
20500
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
20501
|
+
PRIMARY KEY (service, cursor_name)
|
|
20502
|
+
)`
|
|
20503
|
+
];
|
|
20504
|
+
}
|
|
20505
|
+
|
|
20506
|
+
class PostgresTodosSyncStore {
|
|
20507
|
+
client;
|
|
20508
|
+
service;
|
|
20509
|
+
sourceMachineId;
|
|
20510
|
+
tableName;
|
|
20511
|
+
cursorTableName;
|
|
20512
|
+
constructor(client, options = {}) {
|
|
20513
|
+
this.client = client;
|
|
20514
|
+
this.service = options.service ?? "todos";
|
|
20515
|
+
this.sourceMachineId = options.sourceMachineId;
|
|
20516
|
+
this.tableName = options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE;
|
|
20517
|
+
this.cursorTableName = options.cursorTableName ?? DEFAULT_TODOS_POSTGRES_CURSOR_TABLE;
|
|
20518
|
+
assertSafeIdentifier(this.tableName);
|
|
20519
|
+
assertSafeIdentifier(this.cursorTableName);
|
|
20520
|
+
}
|
|
20521
|
+
async ensureSchema() {
|
|
20522
|
+
for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
|
|
20523
|
+
await this.client.query(sql);
|
|
20524
|
+
}
|
|
20525
|
+
}
|
|
20526
|
+
async pushSnapshot(snapshot, context = {}) {
|
|
20527
|
+
const result = { records: 0, objectTypes: {} };
|
|
20528
|
+
const sourceMachineId = context.requestId ?? this.sourceMachineId ?? null;
|
|
20529
|
+
for (const entry of snapshotEntries(snapshot)) {
|
|
20530
|
+
await this.client.query(`INSERT INTO ${this.tableName} (
|
|
20531
|
+
service, object_type, object_id, payload, updated_at,
|
|
20532
|
+
deleted_at, source_machine_id, version
|
|
20533
|
+
) VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, $6::timestamptz, $7, $8)
|
|
20534
|
+
ON CONFLICT (service, object_type, object_id) DO UPDATE SET
|
|
20535
|
+
payload = EXCLUDED.payload,
|
|
20536
|
+
updated_at = EXCLUDED.updated_at,
|
|
20537
|
+
deleted_at = EXCLUDED.deleted_at,
|
|
20538
|
+
source_machine_id = EXCLUDED.source_machine_id,
|
|
20539
|
+
version = EXCLUDED.version
|
|
20540
|
+
WHERE ${this.tableName}.updated_at <= EXCLUDED.updated_at`, [
|
|
20541
|
+
this.service,
|
|
20542
|
+
entry.type,
|
|
20543
|
+
entry.id,
|
|
20544
|
+
JSON.stringify(entry.payload),
|
|
20545
|
+
entry.updatedAt,
|
|
20546
|
+
entry.deletedAt,
|
|
20547
|
+
sourceMachineId,
|
|
20548
|
+
entry.version
|
|
20549
|
+
]);
|
|
20550
|
+
result.records += 1;
|
|
20551
|
+
result.objectTypes[entry.type] = (result.objectTypes[entry.type] ?? 0) + 1;
|
|
20552
|
+
}
|
|
20553
|
+
return result;
|
|
20554
|
+
}
|
|
20555
|
+
async pullSnapshot(options = {}) {
|
|
20556
|
+
const params = [this.service];
|
|
20557
|
+
const filters = ["service = $1", "deleted_at IS NULL"];
|
|
20558
|
+
if (options.since) {
|
|
20559
|
+
params.push(options.since);
|
|
20560
|
+
filters.push(`updated_at > $${params.length}::timestamptz`);
|
|
20561
|
+
}
|
|
20562
|
+
if (options.objectTypes?.length) {
|
|
20563
|
+
params.push(options.objectTypes);
|
|
20564
|
+
filters.push(`object_type = ANY($${params.length}::text[])`);
|
|
20565
|
+
}
|
|
20566
|
+
const response = await this.client.query(`SELECT object_type, payload FROM ${this.tableName}
|
|
20567
|
+
WHERE ${filters.join(" AND ")}
|
|
20568
|
+
ORDER BY updated_at ASC, object_type ASC, object_id ASC`, params);
|
|
20569
|
+
return rowsToSnapshot(response.rows);
|
|
20570
|
+
}
|
|
20571
|
+
async getCursor(name) {
|
|
20572
|
+
const result = await this.client.query(`SELECT value FROM ${this.cursorTableName} WHERE service = $1 AND cursor_name = $2`, [this.service, name]);
|
|
20573
|
+
return result.rows[0]?.value ?? null;
|
|
20574
|
+
}
|
|
20575
|
+
async setCursor(name, value) {
|
|
20576
|
+
await this.client.query(`INSERT INTO ${this.cursorTableName} (service, cursor_name, value, updated_at)
|
|
20577
|
+
VALUES ($1, $2, $3, now())
|
|
20578
|
+
ON CONFLICT (service, cursor_name) DO UPDATE SET
|
|
20579
|
+
value = EXCLUDED.value,
|
|
20580
|
+
updated_at = EXCLUDED.updated_at`, [this.service, name, value]);
|
|
20581
|
+
}
|
|
20582
|
+
}
|
|
20583
|
+
function createPostgresTodosSyncStore(client, options = {}) {
|
|
20584
|
+
return new PostgresTodosSyncStore(client, options);
|
|
20585
|
+
}
|
|
20586
|
+
function snapshotEntries(snapshot) {
|
|
20587
|
+
return [
|
|
20588
|
+
...snapshot.tasks.map((payload) => entry("tasks", payload, snapshot.exportedAt)),
|
|
20589
|
+
...snapshot.projects.map((payload) => entry("projects", payload, snapshot.exportedAt)),
|
|
20590
|
+
...snapshot.plans.map((payload) => entry("plans", payload, snapshot.exportedAt)),
|
|
20591
|
+
...snapshot.agents.map((payload) => entry("agents", payload, snapshot.exportedAt)),
|
|
20592
|
+
...snapshot.taskLists.map((payload) => entry("task_lists", payload, snapshot.exportedAt)),
|
|
20593
|
+
...snapshot.templates.map((payload) => entry("templates", payload, snapshot.exportedAt)),
|
|
20594
|
+
...snapshot.auditHistory.map((payload) => entry("audit_history", payload, snapshot.exportedAt))
|
|
20595
|
+
];
|
|
20596
|
+
}
|
|
20597
|
+
function entry(type, payload, fallbackUpdatedAt) {
|
|
20598
|
+
const id = payload["id"];
|
|
20599
|
+
if (typeof id !== "string" || !id)
|
|
20600
|
+
throw new Error(`${type} record is missing a stable id`);
|
|
20601
|
+
return {
|
|
20602
|
+
type,
|
|
20603
|
+
id,
|
|
20604
|
+
payload,
|
|
20605
|
+
updatedAt: stringValue(payload["updated_at"]) ?? stringValue(payload["created_at"]) ?? fallbackUpdatedAt,
|
|
20606
|
+
deletedAt: stringValue(payload["deleted_at"]),
|
|
20607
|
+
version: numberValue2(payload["version"])
|
|
20608
|
+
};
|
|
20609
|
+
}
|
|
20610
|
+
function rowsToSnapshot(rows) {
|
|
20611
|
+
const snapshot = {
|
|
20612
|
+
exportedAt: new Date().toISOString(),
|
|
20613
|
+
source: "postgres",
|
|
20614
|
+
tasks: [],
|
|
20615
|
+
projects: [],
|
|
20616
|
+
plans: [],
|
|
20617
|
+
agents: [],
|
|
20618
|
+
taskLists: [],
|
|
20619
|
+
templates: [],
|
|
20620
|
+
auditHistory: []
|
|
20621
|
+
};
|
|
20622
|
+
for (const row of rows) {
|
|
20623
|
+
const payload = payloadRecord(row.payload);
|
|
20624
|
+
if (row.object_type === "tasks")
|
|
20625
|
+
snapshot.tasks.push(payload);
|
|
20626
|
+
else if (row.object_type === "projects")
|
|
20627
|
+
snapshot.projects.push(payload);
|
|
20628
|
+
else if (row.object_type === "plans")
|
|
20629
|
+
snapshot.plans.push(payload);
|
|
20630
|
+
else if (row.object_type === "agents")
|
|
20631
|
+
snapshot.agents.push(payload);
|
|
20632
|
+
else if (row.object_type === "task_lists")
|
|
20633
|
+
snapshot.taskLists.push(payload);
|
|
20634
|
+
else if (row.object_type === "templates")
|
|
20635
|
+
snapshot.templates.push(payload);
|
|
20636
|
+
else if (row.object_type === "audit_history")
|
|
20637
|
+
snapshot.auditHistory.push(payload);
|
|
20638
|
+
}
|
|
20639
|
+
return snapshot;
|
|
20640
|
+
}
|
|
20641
|
+
function payloadRecord(value) {
|
|
20642
|
+
if (typeof value === "string")
|
|
20643
|
+
return JSON.parse(value);
|
|
20644
|
+
if (value && typeof value === "object" && !Array.isArray(value))
|
|
20645
|
+
return value;
|
|
20646
|
+
throw new Error("Postgres sync payload must be a JSON object");
|
|
20647
|
+
}
|
|
20648
|
+
function stringValue(value) {
|
|
20649
|
+
return typeof value === "string" && value ? value : null;
|
|
20650
|
+
}
|
|
20651
|
+
function numberValue2(value) {
|
|
20652
|
+
return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
|
|
20653
|
+
}
|
|
20654
|
+
function assertSafeIdentifier(value) {
|
|
20655
|
+
if (!/^[a-z_][a-z0-9_]*$/i.test(value))
|
|
20656
|
+
throw new Error(`Unsafe Postgres identifier: ${value}`);
|
|
20657
|
+
}
|
|
20658
|
+
|
|
20659
|
+
// src/storage/hybrid.ts
|
|
20660
|
+
function createHybridTodosStorageAdapter(options) {
|
|
20661
|
+
const local = options.localAdapter ?? createLocalSqliteTodosStorageAdapter(options.local);
|
|
20662
|
+
const syncStore = options.syncStore ?? (options.postgresClient ? createPostgresTodosSyncStore(options.postgresClient, { sourceMachineId: options.sourceMachineId }) : null);
|
|
20663
|
+
if (!syncStore)
|
|
20664
|
+
throw new Error("hybrid storage requires a Postgres sync store or query client");
|
|
20665
|
+
if (!local.sync.exportSnapshot || !local.sync.importSnapshot) {
|
|
20666
|
+
throw new Error("hybrid storage requires local snapshot export/import support");
|
|
20667
|
+
}
|
|
20668
|
+
const exportSnapshot = async (context) => await local.sync.exportSnapshot(context);
|
|
20669
|
+
const importSnapshot = async (snapshot, context) => await local.sync.importSnapshot(snapshot, context);
|
|
20670
|
+
const remote = {
|
|
20671
|
+
ensureSchema: () => syncStore.ensureSchema(),
|
|
20672
|
+
async pushSnapshot(context) {
|
|
20673
|
+
const snapshot = await exportSnapshot(context);
|
|
20674
|
+
return syncStore.pushSnapshot(snapshot, context);
|
|
20675
|
+
},
|
|
20676
|
+
async pullSnapshot(options2, context) {
|
|
20677
|
+
const snapshot = await syncStore.pullSnapshot(options2);
|
|
20678
|
+
return importSnapshot(snapshot, context);
|
|
20679
|
+
},
|
|
20680
|
+
async syncOnce(options2, context) {
|
|
20681
|
+
const pulled = await remote.pullSnapshot(options2, context);
|
|
20682
|
+
const pushed = await remote.pushSnapshot(context);
|
|
20683
|
+
return { pulled, pushed };
|
|
20684
|
+
}
|
|
20685
|
+
};
|
|
20686
|
+
return {
|
|
20687
|
+
...local,
|
|
20688
|
+
kind: "hybrid",
|
|
20689
|
+
capabilities: {
|
|
20690
|
+
...local.capabilities,
|
|
20691
|
+
localPersistence: true,
|
|
20692
|
+
remotePersistence: true,
|
|
20693
|
+
sync: true
|
|
20694
|
+
},
|
|
20695
|
+
sync: {
|
|
20696
|
+
...local.sync,
|
|
20697
|
+
exportSnapshot,
|
|
20698
|
+
importSnapshot
|
|
20699
|
+
},
|
|
20700
|
+
remote
|
|
20701
|
+
};
|
|
20702
|
+
}
|
|
20703
|
+
|
|
20704
|
+
// src/storage/postgres-adapter.ts
|
|
20705
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
20706
|
+
function createPostgresTodosStorageAdapter(options) {
|
|
20707
|
+
const store = new PostgresJsonRecordStore(options);
|
|
20708
|
+
const adapter = {
|
|
20709
|
+
kind: "postgres",
|
|
20710
|
+
capabilities: {
|
|
20711
|
+
localPersistence: false,
|
|
20712
|
+
remotePersistence: true,
|
|
20713
|
+
transactions: false,
|
|
20714
|
+
auditLog: true,
|
|
20715
|
+
sync: true
|
|
20716
|
+
},
|
|
20717
|
+
tasks: {
|
|
20718
|
+
create: (input, context) => createTask2(input, store, context),
|
|
20719
|
+
get: (id) => store.get("tasks", id),
|
|
20720
|
+
list: (filter = {}) => listTasks2(filter, store),
|
|
20721
|
+
count: async (filter = {}) => (await listTasks2(filter, store)).length,
|
|
20722
|
+
update: (id, input) => updateTask2(id, input, store),
|
|
20723
|
+
delete: (id, context) => store.delete("tasks", id, context),
|
|
20724
|
+
start: (id, agentId) => startTask2(id, agentId, store),
|
|
20725
|
+
complete: (id, agentId, options2) => completeTask2(id, agentId, options2, store),
|
|
20726
|
+
fail: (id, agentId, reason, options2) => failTask2(id, agentId, reason, options2, store),
|
|
20727
|
+
claimNext: (agentId, filters) => claimNextTask2(agentId, filters, store),
|
|
20728
|
+
getNext: (_agentId, filters) => getNextTask2(filters, store),
|
|
20729
|
+
getActiveWork: (filters) => getActiveWork2(filters, store),
|
|
20730
|
+
getChangedSince: (since, filters) => getChangedSince(since, filters, store)
|
|
20731
|
+
},
|
|
20732
|
+
projects: {
|
|
20733
|
+
create: (input, context) => createProject2(input, store, context),
|
|
20734
|
+
get: (id) => store.get("projects", id),
|
|
20735
|
+
getByPath: async (path) => (await store.list("projects")).find((project) => project.path === path) ?? null,
|
|
20736
|
+
list: async () => (await store.list("projects")).sort((a, b) => a.name.localeCompare(b.name)),
|
|
20737
|
+
update: (id, input) => updateProject2(id, input, store),
|
|
20738
|
+
delete: (id, context) => store.delete("projects", id, context)
|
|
20739
|
+
},
|
|
20740
|
+
plans: {
|
|
20741
|
+
create: (input, context) => createPlan2(input, store, context),
|
|
20742
|
+
get: (id) => store.get("plans", id),
|
|
20743
|
+
list: async (projectId) => (await store.list("plans")).filter((plan) => projectId === undefined || plan.project_id === projectId).sort((a, b) => a.name.localeCompare(b.name)),
|
|
20744
|
+
update: (id, input) => updatePlan2(id, input, store),
|
|
20745
|
+
delete: (id, context) => store.delete("plans", id, context)
|
|
20746
|
+
},
|
|
20747
|
+
agents: {
|
|
20748
|
+
register: (input, context) => registerAgent2(input, store, context),
|
|
20749
|
+
get: (id) => store.get("agents", id),
|
|
20750
|
+
getByName: async (name) => (await store.list("agents")).find((agent) => agent.name === name) ?? null,
|
|
20751
|
+
list: async (options2) => (await store.list("agents")).filter((agent) => options2?.include_archived || agent.status !== "archived").sort((a, b) => a.name.localeCompare(b.name)),
|
|
20752
|
+
update: (id, input) => updateAgent2(id, input, store)
|
|
20753
|
+
},
|
|
20754
|
+
taskLists: {
|
|
20755
|
+
create: (input, context) => createTaskList2(input, store, context),
|
|
20756
|
+
get: (id) => store.get("task_lists", id),
|
|
20757
|
+
getBySlug: async (slug, projectId) => (await store.list("task_lists")).find((list) => list.slug === slug && (projectId === undefined || list.project_id === projectId)) ?? null,
|
|
20758
|
+
list: async (projectId) => (await store.list("task_lists")).filter((list) => projectId === undefined || list.project_id === projectId).sort((a, b) => a.name.localeCompare(b.name)),
|
|
20759
|
+
update: (id, input) => updateTaskList2(id, input, store),
|
|
20760
|
+
delete: (id, context) => store.delete("task_lists", id, context)
|
|
20761
|
+
},
|
|
20762
|
+
templates: {
|
|
20763
|
+
create: (input, context) => createTemplate2(input, store, context),
|
|
20764
|
+
get: (id) => store.get("templates", id),
|
|
20765
|
+
list: async () => (await store.list("templates")).sort((a, b) => a.name.localeCompare(b.name)),
|
|
20766
|
+
update: (id, input) => updateTemplate2(id, input, store),
|
|
20767
|
+
delete: (id, context) => store.delete("templates", id, context),
|
|
20768
|
+
getWithTasks: async (id) => {
|
|
20769
|
+
const template = await store.get("templates", id);
|
|
20770
|
+
return template ? { ...template, tasks: [] } : null;
|
|
20771
|
+
}
|
|
20772
|
+
},
|
|
20773
|
+
audit: {
|
|
20774
|
+
logTaskChange: (taskId, action, field2, oldValue, newValue, agentId, context) => logTaskChange2(taskId, action, field2, oldValue, newValue, agentId, store, context),
|
|
20775
|
+
addComment: (input, context) => addComment2(input, store, context),
|
|
20776
|
+
getTaskHistory: async (taskId) => (await store.list("audit_history")).filter((entry2) => entry2.task_id === taskId).sort((a, b) => a.created_at.localeCompare(b.created_at)),
|
|
20777
|
+
getRecentActivity: async (limit = 20) => (await store.list("audit_history")).sort((a, b) => b.created_at.localeCompare(a.created_at)).slice(0, limit)
|
|
20778
|
+
},
|
|
20779
|
+
sync: {
|
|
20780
|
+
getTasksChangedSince: (since, filters) => getChangedSince(since, filters, store),
|
|
20781
|
+
exportSnapshot: () => exportSnapshot(store),
|
|
20782
|
+
importSnapshot: (snapshot, context) => importSnapshot(snapshot, store, context)
|
|
20783
|
+
},
|
|
20784
|
+
transaction: (fn) => fn(adapter)
|
|
20785
|
+
};
|
|
20786
|
+
return adapter;
|
|
20787
|
+
}
|
|
20788
|
+
|
|
20789
|
+
class PostgresJsonRecordStore {
|
|
20790
|
+
options;
|
|
20791
|
+
service;
|
|
20792
|
+
sourceMachineId;
|
|
20793
|
+
tableName;
|
|
20794
|
+
cursorTableName;
|
|
20795
|
+
schemaReady = null;
|
|
20796
|
+
constructor(options) {
|
|
20797
|
+
this.options = options;
|
|
20798
|
+
this.service = options.service ?? "todos";
|
|
20799
|
+
this.sourceMachineId = options.sourceMachineId;
|
|
20800
|
+
this.tableName = options.tableName ?? DEFAULT_TODOS_POSTGRES_SYNC_TABLE;
|
|
20801
|
+
this.cursorTableName = options.cursorTableName ?? DEFAULT_TODOS_POSTGRES_CURSOR_TABLE;
|
|
20802
|
+
}
|
|
20803
|
+
async ensureSchema() {
|
|
20804
|
+
this.schemaReady ??= (async () => {
|
|
20805
|
+
for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
|
|
20806
|
+
await this.options.client.query(sql);
|
|
20807
|
+
}
|
|
20808
|
+
})();
|
|
20809
|
+
await this.schemaReady;
|
|
20810
|
+
}
|
|
20811
|
+
async get(type, id) {
|
|
20812
|
+
await this.ensureSchema();
|
|
20813
|
+
const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at
|
|
20814
|
+
FROM ${this.tableName}
|
|
20815
|
+
WHERE service = $1 AND object_type = $2 AND object_id = $3 AND deleted_at IS NULL
|
|
20816
|
+
LIMIT 1`, [this.service, type, id]);
|
|
20817
|
+
return result.rows[0] ? payloadRecord2(result.rows[0].payload) : null;
|
|
20818
|
+
}
|
|
20819
|
+
async list(type) {
|
|
20820
|
+
return (await this.listRecords(type)).map((record) => record.payload);
|
|
20821
|
+
}
|
|
20822
|
+
async listRecords(type) {
|
|
20823
|
+
await this.ensureSchema();
|
|
20824
|
+
const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at
|
|
20825
|
+
FROM ${this.tableName}
|
|
20826
|
+
WHERE service = $1 AND object_type = $2 AND deleted_at IS NULL
|
|
20827
|
+
ORDER BY updated_at ASC, object_id ASC`, [this.service, type]);
|
|
20828
|
+
return result.rows.map((row) => ({
|
|
20829
|
+
objectId: row.object_id,
|
|
20830
|
+
payload: payloadRecord2(row.payload),
|
|
20831
|
+
updatedAt: stringValue2(row.updated_at) ?? new Date().toISOString()
|
|
20832
|
+
}));
|
|
20833
|
+
}
|
|
20834
|
+
async upsert(type, value, context = {}) {
|
|
20835
|
+
await this.ensureSchema();
|
|
20836
|
+
const updatedAt = stringValue2(value.updated_at) ?? stringValue2(value.created_at) ?? new Date().toISOString();
|
|
20837
|
+
await this.options.client.query(`INSERT INTO ${this.tableName} (
|
|
20838
|
+
service, object_type, object_id, payload, updated_at,
|
|
20839
|
+
deleted_at, source_machine_id, version
|
|
20840
|
+
) VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, NULL, $6, $7)
|
|
20841
|
+
ON CONFLICT (service, object_type, object_id) DO UPDATE SET
|
|
20842
|
+
payload = EXCLUDED.payload,
|
|
20843
|
+
updated_at = EXCLUDED.updated_at,
|
|
20844
|
+
deleted_at = NULL,
|
|
20845
|
+
source_machine_id = EXCLUDED.source_machine_id,
|
|
20846
|
+
version = EXCLUDED.version`, [
|
|
20847
|
+
this.service,
|
|
20848
|
+
type,
|
|
20849
|
+
value.id,
|
|
20850
|
+
JSON.stringify(value),
|
|
20851
|
+
updatedAt,
|
|
20852
|
+
context.requestId ?? this.sourceMachineId ?? null,
|
|
20853
|
+
numberValue3(value.version)
|
|
20854
|
+
]);
|
|
20855
|
+
return value;
|
|
20856
|
+
}
|
|
20857
|
+
async delete(type, id, context = {}) {
|
|
20858
|
+
await this.ensureSchema();
|
|
20859
|
+
const existing = await this.get(type, id);
|
|
20860
|
+
if (!existing)
|
|
20861
|
+
return false;
|
|
20862
|
+
const timestamp2 = new Date().toISOString();
|
|
20863
|
+
await this.options.client.query(`UPDATE ${this.tableName}
|
|
20864
|
+
SET deleted_at = $4::timestamptz,
|
|
20865
|
+
updated_at = $4::timestamptz,
|
|
20866
|
+
source_machine_id = $5
|
|
20867
|
+
WHERE service = $1 AND object_type = $2 AND object_id = $3`, [this.service, type, id, timestamp2, context.requestId ?? this.sourceMachineId ?? null]);
|
|
20868
|
+
return true;
|
|
20869
|
+
}
|
|
20870
|
+
async getCursor(name) {
|
|
20871
|
+
await this.ensureSchema();
|
|
20872
|
+
const result = await this.options.client.query(`SELECT value FROM ${this.cursorTableName} WHERE service = $1 AND cursor_name = $2`, [this.service, name]);
|
|
20873
|
+
return result.rows[0]?.value ?? null;
|
|
20874
|
+
}
|
|
20875
|
+
async setCursor(name, value) {
|
|
20876
|
+
await this.ensureSchema();
|
|
20877
|
+
await this.options.client.query(`INSERT INTO ${this.cursorTableName} (service, cursor_name, value, updated_at)
|
|
20878
|
+
VALUES ($1, $2, $3, now())
|
|
20879
|
+
ON CONFLICT (service, cursor_name) DO UPDATE SET
|
|
20880
|
+
value = EXCLUDED.value,
|
|
20881
|
+
updated_at = EXCLUDED.updated_at`, [this.service, name, value]);
|
|
20882
|
+
}
|
|
20883
|
+
}
|
|
20884
|
+
async function createTask2(input, store, context) {
|
|
20885
|
+
const timestamp2 = new Date().toISOString();
|
|
20886
|
+
const shortId = input.project_id ? await nextTaskShortId2(input.project_id, store, context) : null;
|
|
20887
|
+
const task2 = {
|
|
20888
|
+
id: randomUUID2(),
|
|
20889
|
+
short_id: shortId,
|
|
20890
|
+
project_id: input.project_id ?? context?.projectId ?? null,
|
|
20891
|
+
parent_id: input.parent_id ?? null,
|
|
20892
|
+
plan_id: input.plan_id ?? null,
|
|
20893
|
+
task_list_id: input.task_list_id ?? context?.taskListId ?? null,
|
|
20894
|
+
title: input.title,
|
|
20895
|
+
description: input.description ?? null,
|
|
20896
|
+
status: input.status ?? "pending",
|
|
20897
|
+
priority: input.priority ?? "medium",
|
|
20898
|
+
agent_id: input.agent_id ?? null,
|
|
20899
|
+
assigned_to: input.assigned_to ?? null,
|
|
20900
|
+
session_id: input.session_id ?? context?.sessionId ?? null,
|
|
20901
|
+
working_dir: input.working_dir ?? null,
|
|
20902
|
+
tags: input.tags ?? [],
|
|
20903
|
+
metadata: input.metadata ?? {},
|
|
20904
|
+
version: 1,
|
|
20905
|
+
locked_by: null,
|
|
20906
|
+
locked_at: null,
|
|
20907
|
+
created_at: timestamp2,
|
|
20908
|
+
updated_at: timestamp2,
|
|
20909
|
+
started_at: null,
|
|
20910
|
+
completed_at: null,
|
|
20911
|
+
due_at: input.due_at ?? null,
|
|
20912
|
+
estimated_minutes: input.estimated_minutes ?? null,
|
|
20913
|
+
actual_minutes: null,
|
|
20914
|
+
requires_approval: input.requires_approval ?? false,
|
|
20915
|
+
approved_by: null,
|
|
20916
|
+
approved_at: null,
|
|
20917
|
+
recurrence_rule: input.recurrence_rule ?? null,
|
|
20918
|
+
recurrence_parent_id: input.recurrence_parent_id ?? null,
|
|
20919
|
+
spawns_template_id: input.spawns_template_id ?? null,
|
|
20920
|
+
confidence: input.confidence ?? null,
|
|
20921
|
+
reason: input.reason ?? null,
|
|
20922
|
+
spawned_from_session: input.spawned_from_session ?? null,
|
|
20923
|
+
assigned_by: input.assigned_by ?? null,
|
|
20924
|
+
assigned_from_project: input.assigned_from_project ?? null,
|
|
20925
|
+
task_type: input.task_type ?? null,
|
|
20926
|
+
cost_tokens: 0,
|
|
20927
|
+
cost_usd: 0,
|
|
20928
|
+
delegated_from: null,
|
|
20929
|
+
delegation_depth: 0,
|
|
20930
|
+
retry_count: input.retry_count ?? 0,
|
|
20931
|
+
max_retries: input.max_retries ?? 0,
|
|
20932
|
+
retry_after: input.retry_after ?? null,
|
|
20933
|
+
sla_minutes: input.sla_minutes ?? null,
|
|
20934
|
+
runner_id: null,
|
|
20935
|
+
runner_started_at: null,
|
|
20936
|
+
runner_completed_at: null,
|
|
20937
|
+
current_step: null,
|
|
20938
|
+
total_steps: null
|
|
20939
|
+
};
|
|
20940
|
+
await store.upsert("tasks", task2, context);
|
|
20941
|
+
await logTaskChange2(task2.id, "created", "status", null, task2.status, task2.assigned_by ?? task2.agent_id, store, context);
|
|
20942
|
+
return task2;
|
|
20943
|
+
}
|
|
20944
|
+
async function updateTask2(id, input, store) {
|
|
20945
|
+
const existing = await requireRecord("tasks", id, store);
|
|
20946
|
+
if (existing.version !== input.version) {
|
|
20947
|
+
throw new Error(`Task ${id} version conflict: expected ${existing.version}, got ${input.version}`);
|
|
20948
|
+
}
|
|
20949
|
+
const task2 = {
|
|
20950
|
+
...existing,
|
|
20951
|
+
...definedPatch(input),
|
|
20952
|
+
version: existing.version + 1,
|
|
20953
|
+
updated_at: new Date().toISOString(),
|
|
20954
|
+
tags: input.tags ?? existing.tags,
|
|
20955
|
+
metadata: input.metadata ?? existing.metadata,
|
|
20956
|
+
requires_approval: input.requires_approval ?? existing.requires_approval,
|
|
20957
|
+
task_list_id: input.task_list_id ?? existing.task_list_id
|
|
20958
|
+
};
|
|
20959
|
+
await store.upsert("tasks", task2);
|
|
20960
|
+
return task2;
|
|
20961
|
+
}
|
|
20962
|
+
async function startTask2(id, agentId, store) {
|
|
20963
|
+
const task2 = await requireRecord("tasks", id, store);
|
|
20964
|
+
return patchTask(task2, {
|
|
20965
|
+
status: "in_progress",
|
|
20966
|
+
assigned_to: task2.assigned_to ?? agentId,
|
|
20967
|
+
agent_id: task2.agent_id ?? agentId,
|
|
20968
|
+
locked_by: agentId,
|
|
20969
|
+
locked_at: new Date().toISOString(),
|
|
20970
|
+
started_at: task2.started_at ?? new Date().toISOString()
|
|
20971
|
+
}, store);
|
|
20972
|
+
}
|
|
20973
|
+
async function completeTask2(id, agentId, options, store) {
|
|
20974
|
+
const task2 = await requireRecord("tasks", id, store);
|
|
20975
|
+
return patchTask(task2, {
|
|
20976
|
+
status: "completed",
|
|
20977
|
+
assigned_to: task2.assigned_to ?? agentId ?? null,
|
|
20978
|
+
completed_at: options?.completed_at ?? new Date().toISOString(),
|
|
20979
|
+
actual_minutes: task2.actual_minutes,
|
|
20980
|
+
confidence: options?.confidence ?? task2.confidence
|
|
20981
|
+
}, store);
|
|
20982
|
+
}
|
|
20983
|
+
async function failTask2(id, agentId, reason, options, store) {
|
|
20984
|
+
const task2 = await requireRecord("tasks", id, store);
|
|
20985
|
+
const failed = await patchTask(task2, {
|
|
20986
|
+
status: "failed",
|
|
20987
|
+
assigned_to: task2.assigned_to ?? agentId ?? null,
|
|
20988
|
+
reason: reason ?? task2.reason,
|
|
20989
|
+
retry_after: options?.retry_after ?? task2.retry_after
|
|
20990
|
+
}, store);
|
|
20991
|
+
if (!options?.retry)
|
|
20992
|
+
return { task: failed };
|
|
20993
|
+
const retryTask = await createTask2({
|
|
20994
|
+
title: task2.title,
|
|
20995
|
+
description: task2.description ?? undefined,
|
|
20996
|
+
project_id: task2.project_id ?? undefined,
|
|
20997
|
+
parent_id: task2.parent_id ?? undefined,
|
|
20998
|
+
plan_id: task2.plan_id ?? undefined,
|
|
20999
|
+
task_list_id: task2.task_list_id ?? undefined,
|
|
21000
|
+
priority: task2.priority,
|
|
21001
|
+
assigned_to: task2.assigned_to ?? undefined,
|
|
21002
|
+
tags: task2.tags,
|
|
21003
|
+
metadata: task2.metadata,
|
|
21004
|
+
retry_count: task2.retry_count + 1,
|
|
21005
|
+
max_retries: task2.max_retries,
|
|
21006
|
+
reason: reason ?? undefined,
|
|
21007
|
+
task_type: task2.task_type ?? undefined
|
|
21008
|
+
}, store);
|
|
21009
|
+
return { task: failed, retryTask };
|
|
21010
|
+
}
|
|
21011
|
+
async function patchTask(task2, patch, store) {
|
|
21012
|
+
const updated = {
|
|
21013
|
+
...task2,
|
|
21014
|
+
...patch,
|
|
21015
|
+
version: task2.version + 1,
|
|
21016
|
+
updated_at: new Date().toISOString()
|
|
21017
|
+
};
|
|
21018
|
+
await store.upsert("tasks", updated);
|
|
21019
|
+
return updated;
|
|
21020
|
+
}
|
|
21021
|
+
async function listTasks2(filter, store) {
|
|
21022
|
+
let tasks = await store.list("tasks");
|
|
21023
|
+
tasks = tasks.filter((task2) => taskMatchesFilter(task2, filter));
|
|
21024
|
+
tasks.sort((a, b) => priorityRank(a.priority) - priorityRank(b.priority) || a.created_at.localeCompare(b.created_at));
|
|
21025
|
+
const offset = filter.offset ?? 0;
|
|
21026
|
+
const limit = filter.limit ?? tasks.length;
|
|
21027
|
+
return tasks.slice(offset, offset + limit);
|
|
21028
|
+
}
|
|
21029
|
+
async function getNextTask2(filters, store) {
|
|
21030
|
+
return (await listTasks2({ ...filters, status: "pending", limit: 1 }, store))[0] ?? null;
|
|
21031
|
+
}
|
|
21032
|
+
async function claimNextTask2(agentId, filters, store) {
|
|
21033
|
+
const task2 = await getNextTask2(filters, store);
|
|
21034
|
+
return task2 ? startTask2(task2.id, agentId, store) : null;
|
|
21035
|
+
}
|
|
21036
|
+
async function getActiveWork2(filters, store) {
|
|
21037
|
+
const tasks = await listTasks2({ ...filters, status: "in_progress" }, store);
|
|
21038
|
+
return tasks.map((task2) => ({
|
|
21039
|
+
id: task2.id,
|
|
21040
|
+
short_id: task2.short_id,
|
|
21041
|
+
title: task2.title,
|
|
21042
|
+
priority: task2.priority,
|
|
21043
|
+
assigned_to: task2.assigned_to,
|
|
21044
|
+
locked_by: task2.locked_by,
|
|
21045
|
+
locked_at: task2.locked_at,
|
|
21046
|
+
updated_at: task2.updated_at
|
|
21047
|
+
}));
|
|
21048
|
+
}
|
|
21049
|
+
async function getChangedSince(since, filters, store) {
|
|
21050
|
+
return (await listTasks2(filters ?? {}, store)).filter((task2) => task2.updated_at > since);
|
|
21051
|
+
}
|
|
21052
|
+
async function createProject2(input, store, context) {
|
|
21053
|
+
const timestamp2 = new Date().toISOString();
|
|
21054
|
+
const project = {
|
|
21055
|
+
id: randomUUID2(),
|
|
21056
|
+
name: input.name,
|
|
21057
|
+
path: input.path,
|
|
21058
|
+
description: input.description ?? null,
|
|
21059
|
+
task_list_id: input.task_list_id ?? `todos-${slugify2(input.name)}`,
|
|
21060
|
+
task_prefix: input.task_prefix ?? await generateProjectPrefix(input.name, store),
|
|
21061
|
+
task_counter: 0,
|
|
21062
|
+
created_at: timestamp2,
|
|
21063
|
+
updated_at: timestamp2
|
|
21064
|
+
};
|
|
21065
|
+
return store.upsert("projects", project, context);
|
|
21066
|
+
}
|
|
21067
|
+
async function updateProject2(id, input, store) {
|
|
21068
|
+
const project = await requireRecord("projects", id, store);
|
|
21069
|
+
const updated = { ...project, ...definedPatch(input), updated_at: new Date().toISOString() };
|
|
21070
|
+
return store.upsert("projects", updated);
|
|
21071
|
+
}
|
|
21072
|
+
async function createPlan2(input, store, context) {
|
|
21073
|
+
const timestamp2 = new Date().toISOString();
|
|
21074
|
+
return store.upsert("plans", {
|
|
21075
|
+
id: randomUUID2(),
|
|
21076
|
+
project_id: input.project_id ?? context?.projectId ?? null,
|
|
21077
|
+
task_list_id: input.task_list_id ?? context?.taskListId ?? null,
|
|
21078
|
+
agent_id: input.agent_id ?? context?.agentId ?? null,
|
|
21079
|
+
name: input.name,
|
|
21080
|
+
description: input.description ?? null,
|
|
21081
|
+
status: input.status ?? "active",
|
|
21082
|
+
created_at: timestamp2,
|
|
21083
|
+
updated_at: timestamp2
|
|
21084
|
+
}, context);
|
|
21085
|
+
}
|
|
21086
|
+
async function updatePlan2(id, input, store) {
|
|
21087
|
+
const plan = await requireRecord("plans", id, store);
|
|
21088
|
+
return store.upsert("plans", { ...plan, ...definedPatch(input), updated_at: new Date().toISOString() });
|
|
21089
|
+
}
|
|
21090
|
+
async function registerAgent2(input, store, context) {
|
|
21091
|
+
const existing = (await store.list("agents")).find((agent2) => agent2.name === input.name && agent2.status !== "archived");
|
|
21092
|
+
if (existing && !input.force && existing.session_id && existing.session_id !== input.session_id) {
|
|
21093
|
+
return { conflict: true, message: `Agent name '${input.name}' is already active` };
|
|
21094
|
+
}
|
|
21095
|
+
const timestamp2 = new Date().toISOString();
|
|
21096
|
+
const agent = {
|
|
21097
|
+
id: existing?.id ?? randomUUID2().slice(0, 8),
|
|
21098
|
+
name: input.name,
|
|
21099
|
+
description: input.description ?? existing?.description ?? null,
|
|
21100
|
+
role: input.role ?? existing?.role ?? null,
|
|
21101
|
+
title: input.title ?? existing?.title ?? null,
|
|
21102
|
+
level: input.level ?? existing?.level ?? null,
|
|
21103
|
+
permissions: input.permissions ?? existing?.permissions ?? [],
|
|
21104
|
+
reports_to: input.reports_to ?? existing?.reports_to ?? null,
|
|
21105
|
+
org_id: input.org_id ?? existing?.org_id ?? null,
|
|
21106
|
+
capabilities: input.capabilities ?? existing?.capabilities ?? [],
|
|
21107
|
+
status: "active",
|
|
21108
|
+
metadata: input.metadata ?? existing?.metadata ?? {},
|
|
21109
|
+
created_at: existing?.created_at ?? timestamp2,
|
|
21110
|
+
last_seen_at: timestamp2,
|
|
21111
|
+
session_id: input.session_id ?? context?.sessionId ?? existing?.session_id ?? null,
|
|
21112
|
+
working_dir: input.working_dir ?? existing?.working_dir ?? null,
|
|
21113
|
+
active_project_id: input.project_id ?? context?.projectId ?? existing?.active_project_id ?? null
|
|
21114
|
+
};
|
|
21115
|
+
return store.upsert("agents", agent, context);
|
|
21116
|
+
}
|
|
21117
|
+
async function updateAgent2(id, input, store) {
|
|
21118
|
+
const agent = await store.get("agents", id);
|
|
21119
|
+
if (!agent)
|
|
21120
|
+
return null;
|
|
21121
|
+
return store.upsert("agents", {
|
|
21122
|
+
...agent,
|
|
21123
|
+
...definedPatch(input),
|
|
21124
|
+
permissions: input.permissions ?? agent.permissions,
|
|
21125
|
+
capabilities: input.capabilities ?? agent.capabilities,
|
|
21126
|
+
metadata: input.metadata ?? agent.metadata,
|
|
21127
|
+
last_seen_at: new Date().toISOString()
|
|
21128
|
+
});
|
|
21129
|
+
}
|
|
21130
|
+
async function createTaskList2(input, store, context) {
|
|
21131
|
+
const timestamp2 = new Date().toISOString();
|
|
21132
|
+
return store.upsert("task_lists", {
|
|
21133
|
+
id: randomUUID2(),
|
|
21134
|
+
project_id: input.project_id ?? context?.projectId ?? null,
|
|
21135
|
+
slug: input.slug ?? slugify2(input.name),
|
|
21136
|
+
name: input.name,
|
|
21137
|
+
description: input.description ?? null,
|
|
21138
|
+
metadata: input.metadata ?? {},
|
|
21139
|
+
created_at: timestamp2,
|
|
21140
|
+
updated_at: timestamp2
|
|
21141
|
+
}, context);
|
|
21142
|
+
}
|
|
21143
|
+
async function updateTaskList2(id, input, store) {
|
|
21144
|
+
const list = await requireRecord("task_lists", id, store);
|
|
21145
|
+
return store.upsert("task_lists", {
|
|
21146
|
+
...list,
|
|
21147
|
+
...definedPatch(input),
|
|
21148
|
+
metadata: input.metadata ?? list.metadata,
|
|
21149
|
+
updated_at: new Date().toISOString()
|
|
21150
|
+
});
|
|
21151
|
+
}
|
|
21152
|
+
async function createTemplate2(input, store, context) {
|
|
21153
|
+
const timestamp2 = new Date().toISOString();
|
|
21154
|
+
return store.upsert("templates", {
|
|
21155
|
+
id: randomUUID2(),
|
|
21156
|
+
name: input.name,
|
|
21157
|
+
title_pattern: input.title_pattern,
|
|
21158
|
+
description: input.description ?? null,
|
|
21159
|
+
priority: input.priority ?? "medium",
|
|
21160
|
+
tags: input.tags ?? [],
|
|
21161
|
+
variables: input.variables ?? [],
|
|
21162
|
+
version: 1,
|
|
21163
|
+
project_id: input.project_id ?? context?.projectId ?? null,
|
|
21164
|
+
plan_id: input.plan_id ?? null,
|
|
21165
|
+
metadata: input.metadata ?? {},
|
|
21166
|
+
created_at: timestamp2
|
|
21167
|
+
}, context);
|
|
21168
|
+
}
|
|
21169
|
+
async function updateTemplate2(id, input, store) {
|
|
21170
|
+
const template = await store.get("templates", id);
|
|
21171
|
+
if (!template)
|
|
21172
|
+
return null;
|
|
21173
|
+
return store.upsert("templates", {
|
|
21174
|
+
...template,
|
|
21175
|
+
...definedPatch(input),
|
|
21176
|
+
tags: input.tags ?? template.tags,
|
|
21177
|
+
variables: input.variables ?? template.variables,
|
|
21178
|
+
metadata: input.metadata ?? template.metadata,
|
|
21179
|
+
version: template.version + 1
|
|
21180
|
+
});
|
|
21181
|
+
}
|
|
21182
|
+
async function logTaskChange2(taskId, action, field2, oldValue, newValue, agentId, store, context) {
|
|
21183
|
+
const entry2 = {
|
|
21184
|
+
id: randomUUID2(),
|
|
21185
|
+
task_id: taskId,
|
|
21186
|
+
action,
|
|
21187
|
+
field: field2 ?? null,
|
|
21188
|
+
old_value: oldValue ?? null,
|
|
21189
|
+
new_value: newValue ?? null,
|
|
21190
|
+
agent_id: agentId ?? context?.agentId ?? null,
|
|
21191
|
+
created_at: new Date().toISOString()
|
|
21192
|
+
};
|
|
21193
|
+
return store.upsert("audit_history", entry2, context);
|
|
21194
|
+
}
|
|
21195
|
+
async function addComment2(input, store, context) {
|
|
21196
|
+
const comment = {
|
|
21197
|
+
id: randomUUID2(),
|
|
21198
|
+
task_id: input.task_id,
|
|
21199
|
+
agent_id: input.agent_id ?? context?.agentId ?? null,
|
|
21200
|
+
session_id: input.session_id ?? context?.sessionId ?? null,
|
|
21201
|
+
content: input.content,
|
|
21202
|
+
type: input.type ?? "comment",
|
|
21203
|
+
progress_pct: input.progress_pct ?? null,
|
|
21204
|
+
created_at: new Date().toISOString()
|
|
21205
|
+
};
|
|
21206
|
+
return store.upsert("comments", comment, context);
|
|
21207
|
+
}
|
|
21208
|
+
async function exportSnapshot(store) {
|
|
21209
|
+
return {
|
|
21210
|
+
exportedAt: new Date().toISOString(),
|
|
21211
|
+
source: "postgres",
|
|
21212
|
+
tasks: await store.list("tasks"),
|
|
21213
|
+
projects: await store.list("projects"),
|
|
21214
|
+
plans: await store.list("plans"),
|
|
21215
|
+
agents: await store.list("agents"),
|
|
21216
|
+
taskLists: await store.list("task_lists"),
|
|
21217
|
+
templates: await store.list("templates"),
|
|
21218
|
+
auditHistory: await store.list("audit_history")
|
|
21219
|
+
};
|
|
21220
|
+
}
|
|
21221
|
+
async function importSnapshot(snapshot, store, context) {
|
|
21222
|
+
const result = { inserted: 0, updated: 0, skipped: 0, errors: [] };
|
|
21223
|
+
const entries = [
|
|
21224
|
+
...snapshot.tasks.map((row) => ["tasks", row]),
|
|
21225
|
+
...snapshot.projects.map((row) => ["projects", row]),
|
|
21226
|
+
...snapshot.plans.map((row) => ["plans", row]),
|
|
21227
|
+
...snapshot.agents.map((row) => ["agents", row]),
|
|
21228
|
+
...snapshot.taskLists.map((row) => ["task_lists", row]),
|
|
21229
|
+
...snapshot.templates.map((row) => ["templates", row]),
|
|
21230
|
+
...snapshot.auditHistory.map((row) => ["audit_history", row])
|
|
21231
|
+
];
|
|
21232
|
+
for (const [type, row] of entries) {
|
|
21233
|
+
try {
|
|
21234
|
+
const existing = await store.get(type, row.id);
|
|
21235
|
+
await store.upsert(type, row, context);
|
|
21236
|
+
if (existing)
|
|
21237
|
+
result.updated += 1;
|
|
21238
|
+
else
|
|
21239
|
+
result.inserted += 1;
|
|
21240
|
+
} catch (error) {
|
|
21241
|
+
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
21242
|
+
}
|
|
21243
|
+
}
|
|
21244
|
+
return result;
|
|
21245
|
+
}
|
|
21246
|
+
async function requireRecord(type, id, store) {
|
|
21247
|
+
const record = await store.get(type, id);
|
|
21248
|
+
if (!record)
|
|
21249
|
+
throw new Error(`${type} record not found: ${id}`);
|
|
21250
|
+
return record;
|
|
21251
|
+
}
|
|
21252
|
+
async function nextTaskShortId2(projectId, store, context) {
|
|
21253
|
+
const project = await store.get("projects", projectId);
|
|
21254
|
+
if (!project?.task_prefix)
|
|
21255
|
+
return null;
|
|
21256
|
+
const counter = project.task_counter + 1;
|
|
21257
|
+
await store.upsert("projects", {
|
|
21258
|
+
...project,
|
|
21259
|
+
task_counter: counter,
|
|
21260
|
+
updated_at: new Date().toISOString()
|
|
21261
|
+
}, context);
|
|
21262
|
+
return `${project.task_prefix}-${String(counter).padStart(5, "0")}`;
|
|
21263
|
+
}
|
|
21264
|
+
async function generateProjectPrefix(name, store) {
|
|
21265
|
+
const base = (name.replace(/[^a-zA-Z0-9\s]/g, "").trim().split(/\s+/).filter(Boolean).slice(0, 3).map((word) => word[0]).join("") || name.slice(0, 3) || "TOD").toUpperCase();
|
|
21266
|
+
const existing = new Set((await store.list("projects")).map((project) => project.task_prefix).filter(Boolean));
|
|
21267
|
+
let candidate = base;
|
|
21268
|
+
let suffix = 1;
|
|
21269
|
+
while (existing.has(candidate)) {
|
|
21270
|
+
suffix += 1;
|
|
21271
|
+
candidate = `${base}${suffix}`;
|
|
21272
|
+
}
|
|
21273
|
+
return candidate;
|
|
21274
|
+
}
|
|
21275
|
+
function taskMatchesFilter(task2, filter) {
|
|
21276
|
+
if (filter.ids && !filter.ids.includes(task2.id))
|
|
21277
|
+
return false;
|
|
21278
|
+
if (filter.project_id !== undefined && task2.project_id !== filter.project_id)
|
|
21279
|
+
return false;
|
|
21280
|
+
if (filter.parent_id !== undefined && task2.parent_id !== filter.parent_id)
|
|
21281
|
+
return false;
|
|
21282
|
+
if (filter.plan_id !== undefined && task2.plan_id !== filter.plan_id)
|
|
21283
|
+
return false;
|
|
21284
|
+
if (filter.task_list_id !== undefined && task2.task_list_id !== filter.task_list_id)
|
|
21285
|
+
return false;
|
|
21286
|
+
if (filter.status !== undefined && !matchesOne(task2.status, filter.status))
|
|
21287
|
+
return false;
|
|
21288
|
+
if (filter.priority !== undefined && !matchesOne(task2.priority, filter.priority))
|
|
21289
|
+
return false;
|
|
21290
|
+
if (filter.assigned_to !== undefined && task2.assigned_to !== filter.assigned_to)
|
|
21291
|
+
return false;
|
|
21292
|
+
if (filter.agent_id !== undefined && task2.agent_id !== filter.agent_id)
|
|
21293
|
+
return false;
|
|
21294
|
+
if (filter.session_id !== undefined && task2.session_id !== filter.session_id)
|
|
21295
|
+
return false;
|
|
21296
|
+
if (filter.tags?.length && !filter.tags.every((tag) => task2.tags.includes(tag)))
|
|
21297
|
+
return false;
|
|
21298
|
+
if (filter.has_recurrence !== undefined && Boolean(task2.recurrence_rule) !== filter.has_recurrence)
|
|
21299
|
+
return false;
|
|
21300
|
+
if (filter.include_subtasks !== true && task2.parent_id)
|
|
21301
|
+
return false;
|
|
21302
|
+
if (filter.task_type !== undefined && !matchesOne(task2.task_type ?? "", filter.task_type))
|
|
21303
|
+
return false;
|
|
21304
|
+
return true;
|
|
21305
|
+
}
|
|
21306
|
+
function matchesOne(value, expected) {
|
|
21307
|
+
return Array.isArray(expected) ? expected.includes(value) : value === expected;
|
|
21308
|
+
}
|
|
21309
|
+
function priorityRank(priority) {
|
|
21310
|
+
return { critical: 0, high: 1, medium: 2, low: 3 }[priority];
|
|
21311
|
+
}
|
|
21312
|
+
function slugify2(value) {
|
|
21313
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "todos";
|
|
21314
|
+
}
|
|
21315
|
+
function definedPatch(value) {
|
|
21316
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry2]) => entry2 !== undefined));
|
|
21317
|
+
}
|
|
21318
|
+
function payloadRecord2(value) {
|
|
21319
|
+
if (typeof value === "string")
|
|
21320
|
+
return JSON.parse(value);
|
|
21321
|
+
if (value && typeof value === "object" && !Array.isArray(value))
|
|
21322
|
+
return value;
|
|
21323
|
+
throw new Error("Postgres storage payload must be a JSON object");
|
|
21324
|
+
}
|
|
21325
|
+
function stringValue2(value) {
|
|
21326
|
+
return typeof value === "string" && value ? value : null;
|
|
21327
|
+
}
|
|
21328
|
+
function numberValue3(value) {
|
|
21329
|
+
return typeof value === "number" && Number.isSafeInteger(value) ? value : null;
|
|
21330
|
+
}
|
|
21331
|
+
|
|
21332
|
+
// src/storage/factory.ts
|
|
21333
|
+
function createTodosStorageAdapter(options = {}) {
|
|
21334
|
+
const config = options.config ?? loadTodosStorageConfig(options.env);
|
|
21335
|
+
if (config.mode === "local")
|
|
21336
|
+
return createLocalSqliteTodosStorageAdapter(options.local);
|
|
21337
|
+
assertTodosRemoteStorageConfig(config);
|
|
21338
|
+
const adapter = config.mode === "hybrid" ? options.hybridAdapter ?? createImplicitHybridAdapter(options) ?? options.remoteAdapter : options.remoteAdapter ?? createImplicitPostgresAdapter(options);
|
|
21339
|
+
if (!adapter) {
|
|
21340
|
+
throw new Error(`${config.mode} storage requires a repo-native remote adapter. ` + "Pass remoteAdapter/hybridAdapter after wiring Postgres RDS and S3 support.");
|
|
21341
|
+
}
|
|
21342
|
+
assertRemoteAdapterCapabilities(adapter, config.mode);
|
|
21343
|
+
return adapter;
|
|
21344
|
+
}
|
|
21345
|
+
function createImplicitPostgresAdapter(options) {
|
|
21346
|
+
if (!options.postgresClient)
|
|
21347
|
+
return null;
|
|
21348
|
+
return createPostgresTodosStorageAdapter({
|
|
21349
|
+
client: options.postgresClient,
|
|
21350
|
+
...options.hybrid?.sourceMachineId ? { sourceMachineId: options.hybrid.sourceMachineId } : {}
|
|
21351
|
+
});
|
|
21352
|
+
}
|
|
21353
|
+
function createImplicitHybridAdapter(options) {
|
|
21354
|
+
if (!options.postgresClient && !options.postgresSyncStore)
|
|
21355
|
+
return null;
|
|
21356
|
+
return createHybridTodosStorageAdapter({
|
|
21357
|
+
...options.hybrid ?? {},
|
|
21358
|
+
local: options.local,
|
|
21359
|
+
postgresClient: options.postgresClient,
|
|
21360
|
+
syncStore: options.postgresSyncStore
|
|
21361
|
+
});
|
|
21362
|
+
}
|
|
21363
|
+
function assertRemoteAdapterCapabilities(adapter, mode) {
|
|
21364
|
+
if (!adapter.capabilities.remotePersistence) {
|
|
21365
|
+
throw new Error(`${mode} storage adapter must set capabilities.remotePersistence=true`);
|
|
21366
|
+
}
|
|
21367
|
+
if (mode === "hybrid" && !adapter.capabilities.localPersistence) {
|
|
21368
|
+
throw new Error("hybrid storage adapter must also set capabilities.localPersistence=true");
|
|
21369
|
+
}
|
|
21370
|
+
}
|
|
21371
|
+
// src/storage/s3-artifacts.ts
|
|
21372
|
+
import { createHash as createHash8, createHmac } from "crypto";
|
|
21373
|
+
function createTodosS3ArtifactStore(options) {
|
|
21374
|
+
const requestFetch = options.fetch ?? fetch;
|
|
21375
|
+
const now3 = options.now ?? (() => new Date);
|
|
21376
|
+
return {
|
|
21377
|
+
objectKey: (relativePath) => buildS3ObjectKey(options.config, relativePath),
|
|
21378
|
+
objectUrl: (relativePath) => buildS3ObjectUrl(options.config, buildS3ObjectKey(options.config, relativePath)),
|
|
21379
|
+
async putObject(input) {
|
|
21380
|
+
const key = buildS3ObjectKey(options.config, input.key);
|
|
21381
|
+
const url = buildS3ObjectUrl(options.config, key);
|
|
21382
|
+
const body = normalizeBody(input.body);
|
|
21383
|
+
const headers = {
|
|
21384
|
+
"content-type": input.contentType ?? "application/octet-stream"
|
|
21385
|
+
};
|
|
21386
|
+
for (const [name, value] of Object.entries(input.metadata ?? {})) {
|
|
21387
|
+
headers[`x-amz-meta-${name.toLowerCase()}`] = value;
|
|
21388
|
+
}
|
|
21389
|
+
const signed = signAwsV4Request({
|
|
21390
|
+
method: "PUT",
|
|
21391
|
+
url,
|
|
21392
|
+
region: options.config.region ?? "us-east-1",
|
|
21393
|
+
service: "s3",
|
|
21394
|
+
headers,
|
|
21395
|
+
body,
|
|
21396
|
+
credentials: options.credentials,
|
|
21397
|
+
now: now3()
|
|
21398
|
+
});
|
|
21399
|
+
const response = await requestFetch(url, { method: "PUT", headers: signed.headers, body });
|
|
21400
|
+
if (!response.ok)
|
|
21401
|
+
throw new Error(`S3 put failed with HTTP ${response.status}`);
|
|
21402
|
+
return {
|
|
21403
|
+
bucket: options.config.bucket,
|
|
21404
|
+
key,
|
|
21405
|
+
url: url.toString(),
|
|
21406
|
+
etag: response.headers.get("etag") ?? undefined
|
|
21407
|
+
};
|
|
21408
|
+
},
|
|
21409
|
+
async getObject(relativePath) {
|
|
21410
|
+
const url = buildS3ObjectUrl(options.config, buildS3ObjectKey(options.config, relativePath));
|
|
21411
|
+
const signed = signAwsV4Request({
|
|
21412
|
+
method: "GET",
|
|
21413
|
+
url,
|
|
21414
|
+
region: options.config.region ?? "us-east-1",
|
|
21415
|
+
service: "s3",
|
|
21416
|
+
headers: {},
|
|
21417
|
+
credentials: options.credentials,
|
|
21418
|
+
now: now3()
|
|
21419
|
+
});
|
|
21420
|
+
const response = await requestFetch(url, { method: "GET", headers: signed.headers });
|
|
21421
|
+
if (!response.ok)
|
|
21422
|
+
throw new Error(`S3 get failed with HTTP ${response.status}`);
|
|
21423
|
+
return response;
|
|
21424
|
+
},
|
|
21425
|
+
async deleteObject(relativePath) {
|
|
21426
|
+
const url = buildS3ObjectUrl(options.config, buildS3ObjectKey(options.config, relativePath));
|
|
21427
|
+
const signed = signAwsV4Request({
|
|
21428
|
+
method: "DELETE",
|
|
21429
|
+
url,
|
|
21430
|
+
region: options.config.region ?? "us-east-1",
|
|
21431
|
+
service: "s3",
|
|
21432
|
+
headers: {},
|
|
21433
|
+
credentials: options.credentials,
|
|
21434
|
+
now: now3()
|
|
21435
|
+
});
|
|
21436
|
+
const response = await requestFetch(url, { method: "DELETE", headers: signed.headers });
|
|
21437
|
+
if (!response.ok && response.status !== 404)
|
|
21438
|
+
throw new Error(`S3 delete failed with HTTP ${response.status}`);
|
|
21439
|
+
}
|
|
21440
|
+
};
|
|
21441
|
+
}
|
|
21442
|
+
function buildS3ObjectKey(config, relativePath) {
|
|
21443
|
+
const prefix = normalizeS3Prefix(config.prefix);
|
|
21444
|
+
const key = normalizeS3Key(relativePath);
|
|
21445
|
+
return `${prefix}${key}`;
|
|
21446
|
+
}
|
|
21447
|
+
function buildS3ObjectUrl(config, key) {
|
|
21448
|
+
const encodedKey = encodeS3Path(key);
|
|
21449
|
+
if (config.endpoint || config.forcePathStyle) {
|
|
21450
|
+
const base = new URL(config.endpoint ?? `https://s3.${config.region ?? "us-east-1"}.amazonaws.com`);
|
|
21451
|
+
base.pathname = `${trimTrailingSlash(base.pathname)}/${encodeURIComponent(config.bucket)}/${encodedKey}`;
|
|
21452
|
+
return base;
|
|
21453
|
+
}
|
|
21454
|
+
return new URL(`https://${config.bucket}.s3.${config.region ?? "us-east-1"}.amazonaws.com/${encodedKey}`);
|
|
21455
|
+
}
|
|
21456
|
+
function signAwsV4Request(input) {
|
|
21457
|
+
const amzDate = toAmzDate(input.now);
|
|
21458
|
+
const dateStamp = amzDate.slice(0, 8);
|
|
21459
|
+
const bodyHash = sha256Hex(input.body ?? new Uint8Array);
|
|
21460
|
+
const headers = normalizeHeaders({
|
|
21461
|
+
...input.headers ?? {},
|
|
21462
|
+
host: input.url.host,
|
|
21463
|
+
"x-amz-content-sha256": bodyHash,
|
|
21464
|
+
"x-amz-date": amzDate,
|
|
21465
|
+
...input.credentials.sessionToken ? { "x-amz-security-token": input.credentials.sessionToken } : {}
|
|
21466
|
+
});
|
|
21467
|
+
const signedHeaders = Object.keys(headers).sort().join(";");
|
|
21468
|
+
const canonicalHeaders = Object.keys(headers).sort().map((name) => `${name}:${headers[name]}
|
|
21469
|
+
`).join("");
|
|
21470
|
+
const canonicalRequest = [
|
|
21471
|
+
input.method.toUpperCase(),
|
|
21472
|
+
input.url.pathname || "/",
|
|
21473
|
+
canonicalQuery(input.url),
|
|
21474
|
+
canonicalHeaders,
|
|
21475
|
+
signedHeaders,
|
|
21476
|
+
bodyHash
|
|
21477
|
+
].join(`
|
|
21478
|
+
`);
|
|
21479
|
+
const credentialScope = `${dateStamp}/${input.region}/${input.service}/aws4_request`;
|
|
21480
|
+
const stringToSign = [
|
|
21481
|
+
"AWS4-HMAC-SHA256",
|
|
21482
|
+
amzDate,
|
|
21483
|
+
credentialScope,
|
|
21484
|
+
sha256Hex(canonicalRequest)
|
|
21485
|
+
].join(`
|
|
21486
|
+
`);
|
|
21487
|
+
const signingKey = getSigningKey(input.credentials.secretAccessKey, dateStamp, input.region, input.service);
|
|
21488
|
+
const signature = hmacHex(signingKey, stringToSign);
|
|
21489
|
+
return {
|
|
21490
|
+
headers: {
|
|
21491
|
+
...headers,
|
|
21492
|
+
authorization: [
|
|
21493
|
+
`AWS4-HMAC-SHA256 Credential=${input.credentials.accessKeyId}/${credentialScope}`,
|
|
21494
|
+
`SignedHeaders=${signedHeaders}`,
|
|
21495
|
+
`Signature=${signature}`
|
|
21496
|
+
].join(", ")
|
|
21497
|
+
},
|
|
21498
|
+
canonicalRequest,
|
|
21499
|
+
stringToSign
|
|
21500
|
+
};
|
|
21501
|
+
}
|
|
21502
|
+
function normalizeBody(body) {
|
|
21503
|
+
if (typeof body === "string")
|
|
21504
|
+
return Buffer.from(body);
|
|
21505
|
+
if (body instanceof Uint8Array)
|
|
21506
|
+
return body;
|
|
21507
|
+
if (body instanceof ArrayBuffer)
|
|
21508
|
+
return new Uint8Array(body);
|
|
21509
|
+
throw new Error("S3 body must be a string, Uint8Array, Buffer, or ArrayBuffer");
|
|
21510
|
+
}
|
|
21511
|
+
function normalizeS3Prefix(value) {
|
|
21512
|
+
const normalized = value.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
21513
|
+
if (!normalized || normalized.includes(".."))
|
|
21514
|
+
throw new Error("Invalid S3 object key");
|
|
21515
|
+
return normalized.endsWith("/") ? normalized : `${normalized}/`;
|
|
21516
|
+
}
|
|
21517
|
+
function normalizeS3Key(value) {
|
|
21518
|
+
const normalized = value.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
21519
|
+
if (!normalized || normalized.includes("..") || normalized.endsWith("/"))
|
|
21520
|
+
throw new Error("Invalid S3 object key");
|
|
21521
|
+
return normalized;
|
|
21522
|
+
}
|
|
21523
|
+
function encodeS3Path(key) {
|
|
21524
|
+
return key.split("/").filter(Boolean).map((part) => encodeURIComponent(part)).join("/");
|
|
21525
|
+
}
|
|
21526
|
+
function trimTrailingSlash(value) {
|
|
21527
|
+
return value.replace(/\/+$/, "");
|
|
21528
|
+
}
|
|
21529
|
+
function canonicalQuery(url) {
|
|
21530
|
+
return [...url.searchParams.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
|
|
21531
|
+
}
|
|
21532
|
+
function normalizeHeaders(headers) {
|
|
21533
|
+
const entries = Object.entries(headers).map(([name, value]) => [
|
|
21534
|
+
name.toLowerCase(),
|
|
21535
|
+
value.trim().replace(/\s+/g, " ")
|
|
21536
|
+
]);
|
|
21537
|
+
entries.sort((left, right) => left[0].localeCompare(right[0]));
|
|
21538
|
+
return Object.fromEntries(entries);
|
|
21539
|
+
}
|
|
21540
|
+
function toAmzDate(date) {
|
|
21541
|
+
return date.toISOString().replace(/[:-]|\.\d{3}/g, "");
|
|
21542
|
+
}
|
|
21543
|
+
function sha256Hex(value) {
|
|
21544
|
+
return createHash8("sha256").update(value).digest("hex");
|
|
21545
|
+
}
|
|
21546
|
+
function hmac(key, value) {
|
|
21547
|
+
return createHmac("sha256", key).update(value).digest();
|
|
21548
|
+
}
|
|
21549
|
+
function hmacHex(key, value) {
|
|
21550
|
+
return createHmac("sha256", key).update(value).digest("hex");
|
|
21551
|
+
}
|
|
21552
|
+
function getSigningKey(secretAccessKey, dateStamp, region, service) {
|
|
21553
|
+
const dateKey = hmac(`AWS4${secretAccessKey}`, dateStamp);
|
|
21554
|
+
const dateRegionKey = hmac(dateKey, region);
|
|
21555
|
+
const dateRegionServiceKey = hmac(dateRegionKey, service);
|
|
21556
|
+
return hmac(dateRegionServiceKey, "aws4_request");
|
|
21557
|
+
}
|
|
21558
|
+
// src/storage/s3-artifact-sync.ts
|
|
21559
|
+
init_database();
|
|
21560
|
+
async function uploadRunArtifactsToS3(options) {
|
|
21561
|
+
const db = options.db ?? getDatabase();
|
|
21562
|
+
const now3 = options.now ?? (() => new Date);
|
|
21563
|
+
const result = emptyResult();
|
|
21564
|
+
for (const artifact of listRunArtifacts(db, options.filter)) {
|
|
21565
|
+
try {
|
|
21566
|
+
const metadata = artifact.metadata;
|
|
21567
|
+
if (!options.filter?.includeAlreadySynced && remoteRef(metadata)) {
|
|
21568
|
+
result.skipped += 1;
|
|
21569
|
+
continue;
|
|
21570
|
+
}
|
|
21571
|
+
const content = exportStoredArtifactContent({
|
|
21572
|
+
id: artifact.id,
|
|
21573
|
+
path: artifact.path,
|
|
21574
|
+
size_bytes: artifact.size_bytes,
|
|
21575
|
+
sha256: artifact.sha256,
|
|
21576
|
+
metadata
|
|
21577
|
+
});
|
|
21578
|
+
if (!content) {
|
|
21579
|
+
result.skipped += 1;
|
|
21580
|
+
continue;
|
|
21581
|
+
}
|
|
21582
|
+
const ref = await options.store.putObject({
|
|
21583
|
+
key: content.relative_path,
|
|
21584
|
+
body: Buffer.from(content.base64, "base64"),
|
|
21585
|
+
contentType: mediaType(metadata) ?? "application/octet-stream",
|
|
21586
|
+
metadata: {
|
|
21587
|
+
artifact_id: artifact.id,
|
|
21588
|
+
run_id: artifact.run_id,
|
|
21589
|
+
task_id: artifact.task_id,
|
|
21590
|
+
sha256: content.sha256
|
|
21591
|
+
}
|
|
21592
|
+
});
|
|
21593
|
+
const remote = {
|
|
21594
|
+
provider: "s3",
|
|
21595
|
+
bucket: ref.bucket,
|
|
21596
|
+
key: ref.key,
|
|
21597
|
+
relative_path: content.relative_path,
|
|
21598
|
+
url: ref.url,
|
|
21599
|
+
sha256: content.sha256,
|
|
21600
|
+
size_bytes: content.size_bytes,
|
|
21601
|
+
uploaded_at: now3().toISOString()
|
|
21602
|
+
};
|
|
21603
|
+
updateArtifactMetadata(db, artifact.id, {
|
|
21604
|
+
...metadata,
|
|
21605
|
+
remote_artifact_store: remote
|
|
21606
|
+
});
|
|
21607
|
+
result.uploaded += 1;
|
|
21608
|
+
result.artifacts.push({
|
|
21609
|
+
id: artifact.id,
|
|
21610
|
+
run_id: artifact.run_id,
|
|
21611
|
+
task_id: artifact.task_id,
|
|
21612
|
+
key: ref.key,
|
|
21613
|
+
sha256: content.sha256,
|
|
21614
|
+
size_bytes: content.size_bytes
|
|
21615
|
+
});
|
|
21616
|
+
} catch (error) {
|
|
21617
|
+
result.errors.push(`${artifact.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
21618
|
+
}
|
|
21619
|
+
}
|
|
21620
|
+
return result;
|
|
21621
|
+
}
|
|
21622
|
+
function planRunArtifactsS3Sync(options) {
|
|
21623
|
+
const db = options.db ?? getDatabase();
|
|
21624
|
+
const plan = {
|
|
21625
|
+
direction: options.direction,
|
|
21626
|
+
dry_run: true,
|
|
21627
|
+
no_network: true,
|
|
21628
|
+
total: 0,
|
|
21629
|
+
uploadable: 0,
|
|
21630
|
+
downloadable: 0,
|
|
21631
|
+
skipped: 0,
|
|
21632
|
+
errors: [],
|
|
21633
|
+
artifacts: []
|
|
21634
|
+
};
|
|
21635
|
+
for (const artifact of listRunArtifacts(db, options.filter)) {
|
|
21636
|
+
plan.total += 1;
|
|
21637
|
+
try {
|
|
21638
|
+
const metadata = artifact.metadata;
|
|
21639
|
+
const remote = remoteRef(metadata);
|
|
21640
|
+
const integrity = verifyStoredArtifact({
|
|
21641
|
+
id: artifact.id,
|
|
21642
|
+
path: artifact.path,
|
|
21643
|
+
size_bytes: artifact.size_bytes,
|
|
21644
|
+
sha256: artifact.sha256,
|
|
21645
|
+
metadata
|
|
21646
|
+
});
|
|
21647
|
+
if (options.direction === "upload") {
|
|
21648
|
+
if (remote && !options.filter?.includeAlreadySynced) {
|
|
21649
|
+
plan.skipped += 1;
|
|
21650
|
+
plan.artifacts.push(planArtifact(artifact, "already_remote", remote));
|
|
21651
|
+
} else if (integrity.status === "ok") {
|
|
21652
|
+
plan.uploadable += 1;
|
|
21653
|
+
plan.artifacts.push(planArtifact(artifact, "uploadable", remote));
|
|
21654
|
+
} else {
|
|
21655
|
+
plan.skipped += 1;
|
|
21656
|
+
plan.artifacts.push(planArtifact(artifact, integrity.status === "metadata_only" ? "metadata_only" : "missing_local", remote));
|
|
21657
|
+
}
|
|
21658
|
+
} else if (!remote) {
|
|
21659
|
+
plan.skipped += 1;
|
|
21660
|
+
plan.artifacts.push(planArtifact(artifact, "missing_remote_ref", remote));
|
|
21661
|
+
} else if (!options.force && integrity.status === "ok") {
|
|
21662
|
+
plan.skipped += 1;
|
|
21663
|
+
plan.artifacts.push(planArtifact(artifact, "local_ok", remote));
|
|
21664
|
+
} else {
|
|
21665
|
+
plan.downloadable += 1;
|
|
21666
|
+
plan.artifacts.push(planArtifact(artifact, "downloadable", remote));
|
|
21667
|
+
}
|
|
21668
|
+
} catch (error) {
|
|
21669
|
+
plan.errors.push(`${artifact.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
21670
|
+
}
|
|
21671
|
+
}
|
|
21672
|
+
return plan;
|
|
21673
|
+
}
|
|
21674
|
+
async function downloadRunArtifactsFromS3(options) {
|
|
21675
|
+
const db = options.db ?? getDatabase();
|
|
21676
|
+
const now3 = options.now ?? (() => new Date);
|
|
21677
|
+
const result = emptyResult();
|
|
21678
|
+
for (const artifact of listRunArtifacts(db, options.filter)) {
|
|
21679
|
+
try {
|
|
21680
|
+
const metadata = artifact.metadata;
|
|
21681
|
+
const remote = remoteRef(metadata);
|
|
21682
|
+
if (!remote) {
|
|
21683
|
+
result.skipped += 1;
|
|
21684
|
+
continue;
|
|
21685
|
+
}
|
|
21686
|
+
const integrity = verifyStoredArtifact({
|
|
21687
|
+
id: artifact.id,
|
|
21688
|
+
path: artifact.path,
|
|
21689
|
+
size_bytes: artifact.size_bytes,
|
|
21690
|
+
sha256: artifact.sha256,
|
|
21691
|
+
metadata
|
|
21692
|
+
});
|
|
21693
|
+
if (!options.force && integrity.status === "ok") {
|
|
21694
|
+
result.skipped += 1;
|
|
21695
|
+
continue;
|
|
21696
|
+
}
|
|
21697
|
+
const response = await options.store.getObject(remote.relative_path);
|
|
21698
|
+
const bytes = Buffer.from(await response.arrayBuffer());
|
|
21699
|
+
const report = importStoredArtifactContent({
|
|
21700
|
+
artifact_id: artifact.id,
|
|
21701
|
+
relative_path: remote.relative_path,
|
|
21702
|
+
sha256: remote.sha256,
|
|
21703
|
+
size_bytes: remote.size_bytes,
|
|
21704
|
+
base64: bytes.toString("base64")
|
|
21705
|
+
});
|
|
21706
|
+
if (report.status !== "ok")
|
|
21707
|
+
throw new Error(report.message);
|
|
21708
|
+
updateArtifactMetadata(db, artifact.id, {
|
|
21709
|
+
...metadata,
|
|
21710
|
+
remote_artifact_store: {
|
|
21711
|
+
...remote,
|
|
21712
|
+
downloaded_at: now3().toISOString()
|
|
21713
|
+
}
|
|
21714
|
+
});
|
|
21715
|
+
result.downloaded += 1;
|
|
21716
|
+
result.artifacts.push({
|
|
21717
|
+
id: artifact.id,
|
|
21718
|
+
run_id: artifact.run_id,
|
|
21719
|
+
task_id: artifact.task_id,
|
|
21720
|
+
key: remote.key,
|
|
21721
|
+
sha256: remote.sha256,
|
|
21722
|
+
size_bytes: remote.size_bytes
|
|
21723
|
+
});
|
|
21724
|
+
} catch (error) {
|
|
21725
|
+
result.errors.push(`${artifact.id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
21726
|
+
}
|
|
21727
|
+
}
|
|
21728
|
+
return result;
|
|
21729
|
+
}
|
|
21730
|
+
function emptyResult() {
|
|
21731
|
+
return { uploaded: 0, downloaded: 0, skipped: 0, errors: [], artifacts: [] };
|
|
21732
|
+
}
|
|
21733
|
+
function planArtifact(artifact, status, remote) {
|
|
21734
|
+
return {
|
|
21735
|
+
id: artifact.id,
|
|
21736
|
+
run_id: artifact.run_id,
|
|
21737
|
+
task_id: artifact.task_id,
|
|
21738
|
+
status,
|
|
21739
|
+
sha256: artifact.sha256,
|
|
21740
|
+
size_bytes: artifact.size_bytes,
|
|
21741
|
+
...remote ? { remote_key: remote.key } : {}
|
|
21742
|
+
};
|
|
21743
|
+
}
|
|
21744
|
+
function listRunArtifacts(db, filter = {}) {
|
|
21745
|
+
const conditions = [];
|
|
21746
|
+
const values = [];
|
|
21747
|
+
if (filter.runId) {
|
|
21748
|
+
conditions.push("run_id = ?");
|
|
21749
|
+
values.push(filter.runId);
|
|
21750
|
+
}
|
|
21751
|
+
if (filter.taskId) {
|
|
21752
|
+
conditions.push("task_id = ?");
|
|
21753
|
+
values.push(filter.taskId);
|
|
21754
|
+
}
|
|
21755
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
21756
|
+
const limit = filter.limit && filter.limit > 0 ? " LIMIT ?" : "";
|
|
21757
|
+
const rows = db.query(`SELECT * FROM task_run_artifacts ${where} ORDER BY created_at, id${limit}`).all(...limit ? [...values, String(filter.limit)] : values);
|
|
21758
|
+
return rows.map((row) => ({ ...row, metadata: parseMetadata4(row.metadata) }));
|
|
21759
|
+
}
|
|
21760
|
+
function updateArtifactMetadata(db, id, metadata) {
|
|
21761
|
+
db.run("UPDATE task_run_artifacts SET metadata = ? WHERE id = ?", [JSON.stringify(metadata), id]);
|
|
21762
|
+
}
|
|
21763
|
+
function parseMetadata4(value) {
|
|
21764
|
+
if (!value)
|
|
21765
|
+
return {};
|
|
21766
|
+
if (typeof value === "object" && !Array.isArray(value))
|
|
21767
|
+
return value;
|
|
21768
|
+
if (typeof value !== "string")
|
|
21769
|
+
return {};
|
|
21770
|
+
try {
|
|
21771
|
+
const parsed = JSON.parse(value);
|
|
21772
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
21773
|
+
} catch {
|
|
21774
|
+
return {};
|
|
21775
|
+
}
|
|
21776
|
+
}
|
|
21777
|
+
function remoteRef(metadata) {
|
|
21778
|
+
const value = metadata["remote_artifact_store"];
|
|
21779
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
21780
|
+
return null;
|
|
21781
|
+
const record = value;
|
|
21782
|
+
if (record.provider !== "s3")
|
|
21783
|
+
return null;
|
|
21784
|
+
if (typeof record.bucket !== "string" || typeof record.key !== "string" || typeof record.relative_path !== "string" || typeof record.url !== "string" || typeof record.sha256 !== "string" || typeof record.size_bytes !== "number") {
|
|
21785
|
+
return null;
|
|
21786
|
+
}
|
|
21787
|
+
return record;
|
|
21788
|
+
}
|
|
21789
|
+
function mediaType(metadata) {
|
|
21790
|
+
const store = metadata["artifact_store"];
|
|
21791
|
+
if (!store || typeof store !== "object" || Array.isArray(store))
|
|
21792
|
+
return null;
|
|
21793
|
+
const value = store["media_type"];
|
|
21794
|
+
return typeof value === "string" ? value : null;
|
|
21795
|
+
}
|
|
19777
21796
|
// src/sdk/types.ts
|
|
19778
21797
|
class TodosAPIError extends Error {
|
|
19779
21798
|
status;
|
|
@@ -20333,6 +22352,132 @@ class TodosClient {
|
|
|
20333
22352
|
function createClient(options) {
|
|
20334
22353
|
return new TodosClient(options);
|
|
20335
22354
|
}
|
|
22355
|
+
// src/lib/native-storage-status.ts
|
|
22356
|
+
function getNativeStorageStatus(env = process.env) {
|
|
22357
|
+
const issues = [];
|
|
22358
|
+
const warnings = [];
|
|
22359
|
+
let config;
|
|
22360
|
+
try {
|
|
22361
|
+
config = loadTodosStorageConfig(env);
|
|
22362
|
+
} catch (error) {
|
|
22363
|
+
issues.push(error instanceof Error ? error.message : String(error));
|
|
22364
|
+
config = fallbackConfig(env);
|
|
22365
|
+
}
|
|
22366
|
+
try {
|
|
22367
|
+
assertTodosRemoteStorageConfig(config);
|
|
22368
|
+
} catch (error) {
|
|
22369
|
+
issues.push(error instanceof Error ? error.message : String(error));
|
|
22370
|
+
}
|
|
22371
|
+
const remoteEnabled = isTodosRemoteStorageEnabled(config);
|
|
22372
|
+
const remoteFieldsConfigured = Boolean(config.database || config.objectStorage);
|
|
22373
|
+
if (!remoteEnabled && remoteFieldsConfigured) {
|
|
22374
|
+
warnings.push(`${TODOS_STORAGE_ENV.mode}=local ignores configured remote storage fields`);
|
|
22375
|
+
}
|
|
22376
|
+
if (remoteEnabled && !config.objectStorage) {
|
|
22377
|
+
warnings.push(`${TODOS_STORAGE_ENV.s3Bucket} is not configured, so artifact sync will stay local`);
|
|
22378
|
+
}
|
|
22379
|
+
return {
|
|
22380
|
+
ok: issues.length === 0,
|
|
22381
|
+
service: "todos",
|
|
22382
|
+
mode: config.mode,
|
|
22383
|
+
local_default: config.mode === "local",
|
|
22384
|
+
remote_enabled: remoteEnabled,
|
|
22385
|
+
database: {
|
|
22386
|
+
configured: Boolean(config.database),
|
|
22387
|
+
provider: config.database?.provider ?? null,
|
|
22388
|
+
redacted_url: redactDatabaseUrl(config.database?.url),
|
|
22389
|
+
ssl: config.database?.ssl ?? null,
|
|
22390
|
+
schema: config.database?.schema ?? null
|
|
22391
|
+
},
|
|
22392
|
+
object_storage: {
|
|
22393
|
+
configured: Boolean(config.objectStorage),
|
|
22394
|
+
provider: config.objectStorage?.provider ?? null,
|
|
22395
|
+
bucket: config.objectStorage?.bucket ?? null,
|
|
22396
|
+
prefix: config.objectStorage?.prefix ?? null,
|
|
22397
|
+
region: config.objectStorage?.region ?? null,
|
|
22398
|
+
endpoint_configured: Boolean(config.objectStorage?.endpoint),
|
|
22399
|
+
force_path_style: config.objectStorage?.forcePathStyle ?? false
|
|
22400
|
+
},
|
|
22401
|
+
sync: {
|
|
22402
|
+
batch_size: config.sync.batchSize,
|
|
22403
|
+
dry_run: config.sync.dryRun
|
|
22404
|
+
},
|
|
22405
|
+
env: storageEnvStatus(env),
|
|
22406
|
+
canonical: getCanonicalTodosRdsConfig(),
|
|
22407
|
+
issues,
|
|
22408
|
+
warnings,
|
|
22409
|
+
no_network: true
|
|
22410
|
+
};
|
|
22411
|
+
}
|
|
22412
|
+
function getNativeStorageSyncPlan(env = process.env, options = {}) {
|
|
22413
|
+
const status = getNativeStorageStatus(env);
|
|
22414
|
+
const steps = [
|
|
22415
|
+
"Read local SQLite snapshot",
|
|
22416
|
+
status.remote_enabled ? "Prepare Postgres sync table upserts" : "Skip remote database writes in local mode",
|
|
22417
|
+
status.object_storage.configured ? "Plan S3 artifact object writes" : "Keep artifact storage local",
|
|
22418
|
+
"Report planned changes without opening network connections"
|
|
22419
|
+
];
|
|
22420
|
+
return {
|
|
22421
|
+
ok: status.ok,
|
|
22422
|
+
service: "todos",
|
|
22423
|
+
dry_run: true,
|
|
22424
|
+
no_network: true,
|
|
22425
|
+
status,
|
|
22426
|
+
postgres: {
|
|
22427
|
+
required: status.remote_enabled,
|
|
22428
|
+
configured: status.database.configured,
|
|
22429
|
+
schema_sql: options.includeSchemaSql ? postgresTodosSyncSchemaSql() : []
|
|
22430
|
+
},
|
|
22431
|
+
object_storage: {
|
|
22432
|
+
required: status.remote_enabled,
|
|
22433
|
+
configured: status.object_storage.configured,
|
|
22434
|
+
bucket: status.object_storage.bucket,
|
|
22435
|
+
prefix: status.object_storage.prefix
|
|
22436
|
+
},
|
|
22437
|
+
steps
|
|
22438
|
+
};
|
|
22439
|
+
}
|
|
22440
|
+
function redactDatabaseUrl(value) {
|
|
22441
|
+
if (!value)
|
|
22442
|
+
return null;
|
|
22443
|
+
try {
|
|
22444
|
+
const url = new URL(value);
|
|
22445
|
+
if (url.username)
|
|
22446
|
+
url.username = "***";
|
|
22447
|
+
if (url.password)
|
|
22448
|
+
url.password = "***";
|
|
22449
|
+
return url.toString();
|
|
22450
|
+
} catch {
|
|
22451
|
+
return "(redacted)";
|
|
22452
|
+
}
|
|
22453
|
+
}
|
|
22454
|
+
function storageEnvStatus(env) {
|
|
22455
|
+
return Object.fromEntries(Object.entries(TODOS_STORAGE_ENV).map(([key, name]) => [
|
|
22456
|
+
key,
|
|
22457
|
+
{
|
|
22458
|
+
name,
|
|
22459
|
+
active_name: getTodosStorageEnvName(env, key),
|
|
22460
|
+
configured: Boolean(clean2(env[getTodosStorageEnvName(env, key)]))
|
|
22461
|
+
}
|
|
22462
|
+
]));
|
|
22463
|
+
}
|
|
22464
|
+
function fallbackConfig(env) {
|
|
22465
|
+
const modeValue = clean2(env[TODOS_STORAGE_ENV.mode]);
|
|
22466
|
+
const mode = modeValue === "remote" || modeValue === "hybrid" ? modeValue : "local";
|
|
22467
|
+
return {
|
|
22468
|
+
service: "todos",
|
|
22469
|
+
mode,
|
|
22470
|
+
sync: {
|
|
22471
|
+
batchSize: 500,
|
|
22472
|
+
dryRun: false
|
|
22473
|
+
}
|
|
22474
|
+
};
|
|
22475
|
+
}
|
|
22476
|
+
function clean2(value) {
|
|
22477
|
+
const trimmed = value?.trim();
|
|
22478
|
+
return trimmed ? trimmed : undefined;
|
|
22479
|
+
}
|
|
22480
|
+
|
|
20336
22481
|
// src/index.ts
|
|
20337
22482
|
init_database();
|
|
20338
22483
|
|
|
@@ -21101,7 +23246,7 @@ function scoreHealth(scope, scopeId, db) {
|
|
|
21101
23246
|
JOIN tasks dep ON dep.id = td.depends_on
|
|
21102
23247
|
WHERE td.task_id = ? AND dep.status != 'completed'`).all(task2.id);
|
|
21103
23248
|
return { id: task2.id, short_id: task2.short_id, title: redactEvidenceText(task2.title), blockers };
|
|
21104
|
-
}).filter((
|
|
23249
|
+
}).filter((entry2) => entry2.blockers.length > 0);
|
|
21105
23250
|
const overdue = tasks.filter((task2) => activeTaskIds.has(task2.id) && Boolean(task2.due_at && task2.due_at < generatedAt)).map((task2) => ({ id: task2.id, short_id: task2.short_id, title: redactEvidenceText(task2.title), due_at: task2.due_at }));
|
|
21106
23251
|
const failedChecks = d.query(`SELECT tv.id, tv.task_id, tv.command, tv.run_at
|
|
21107
23252
|
FROM task_verifications tv
|
|
@@ -21656,7 +23801,7 @@ function getProjectByPathForBootstrap(path, db) {
|
|
|
21656
23801
|
}
|
|
21657
23802
|
// src/db/api-keys.ts
|
|
21658
23803
|
init_database();
|
|
21659
|
-
import { createHash as
|
|
23804
|
+
import { createHash as createHash9, randomBytes as randomBytes2, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
21660
23805
|
function rowToRecord(row) {
|
|
21661
23806
|
return {
|
|
21662
23807
|
id: row.id,
|
|
@@ -21670,7 +23815,7 @@ function rowToRecord(row) {
|
|
|
21670
23815
|
};
|
|
21671
23816
|
}
|
|
21672
23817
|
function hashApiKey(key) {
|
|
21673
|
-
return
|
|
23818
|
+
return createHash9("sha256").update(key).digest("hex");
|
|
21674
23819
|
}
|
|
21675
23820
|
function safeEqualHex(a, b) {
|
|
21676
23821
|
if (a.length !== b.length)
|
|
@@ -22023,11 +24168,11 @@ function exportArtifacts(filter = {}, db, dbPath) {
|
|
|
22023
24168
|
}));
|
|
22024
24169
|
return buildArtifactExportManifest(artifacts, dbPath);
|
|
22025
24170
|
}
|
|
22026
|
-
function importArtifactFromManifestEntry(
|
|
22027
|
-
const artifact = addArtifact(
|
|
22028
|
-
if (
|
|
24171
|
+
function importArtifactFromManifestEntry(entry2, db, dbPath) {
|
|
24172
|
+
const artifact = addArtifact(entry2, db, dbPath);
|
|
24173
|
+
if (entry2.content_hash && artifact.content_hash !== entry2.content_hash) {
|
|
22029
24174
|
purgeArtifact(artifact.id, db, dbPath);
|
|
22030
|
-
throw new Error(`Content hash mismatch for ${
|
|
24175
|
+
throw new Error(`Content hash mismatch for ${entry2.name}: expected ${entry2.content_hash}, got ${artifact.content_hash}`);
|
|
22031
24176
|
}
|
|
22032
24177
|
return artifact;
|
|
22033
24178
|
}
|
|
@@ -22038,11 +24183,16 @@ var FORBIDDEN_HOSTED_HOSTS = [
|
|
|
22038
24183
|
"www.todos.md",
|
|
22039
24184
|
"preview.todos.md",
|
|
22040
24185
|
"pay.hasna.tools",
|
|
22041
|
-
"platform
|
|
24186
|
+
["platform", "todos"].join("-")
|
|
22042
24187
|
];
|
|
24188
|
+
var PRIVATE_PLATFORM_ORG = ["hasna", "studio"].join("");
|
|
24189
|
+
var HOSTED_TODOS_PACKAGE = ["platform", "todos"].join("-");
|
|
22043
24190
|
var FORBIDDEN_WEB_PATTERNS = [
|
|
22044
24191
|
{ name: "hosted todos.md API", pattern: /https?:\/\/(?:www\.)?todos\.md/i },
|
|
22045
|
-
{
|
|
24192
|
+
{
|
|
24193
|
+
name: "hosted platform package",
|
|
24194
|
+
pattern: new RegExp(`@${PRIVATE_PLATFORM_ORG}/${HOSTED_TODOS_PACKAGE}|${PRIVATE_PLATFORM_ORG}/${HOSTED_TODOS_PACKAGE}`, "i")
|
|
24195
|
+
},
|
|
22046
24196
|
{ name: "browser sign-in flow", pattern: /\/sign-?in\b|\/login\b.*(?:oauth|session|auth)/i },
|
|
22047
24197
|
{ name: "Stripe billing UI", pattern: /\bstripe\.(?:com|js)\b|\bcheckout\.sessions?\b/i },
|
|
22048
24198
|
{ name: "hosted OAuth redirect", pattern: /oauth.*redirect.*todos\.md/i }
|
|
@@ -22095,7 +24245,7 @@ function getHeadlessBoundaryManifest() {
|
|
|
22095
24245
|
notes: [
|
|
22096
24246
|
"Use todos CLI or todos-mcp for agent workflows.",
|
|
22097
24247
|
"todos serve exposes a local-only REST API on 127.0.0.1 \u2014 not a hosted SaaS.",
|
|
22098
|
-
"
|
|
24248
|
+
"Remote sync is explicit opt-in from CLI/MCP, never from the dashboard."
|
|
22099
24249
|
]
|
|
22100
24250
|
};
|
|
22101
24251
|
}
|
|
@@ -22832,10 +24982,10 @@ async function runVerificationProvider(input, db) {
|
|
|
22832
24982
|
}
|
|
22833
24983
|
return result;
|
|
22834
24984
|
}
|
|
22835
|
-
function
|
|
24985
|
+
function taskVerificationRowToRecord(row) {
|
|
22836
24986
|
return {
|
|
22837
24987
|
id: String(row["id"]),
|
|
22838
|
-
task_id:
|
|
24988
|
+
task_id: row["task_id"] ?? null,
|
|
22839
24989
|
command: String(row["command"] ?? ""),
|
|
22840
24990
|
status: row["status"] ?? "unknown",
|
|
22841
24991
|
output_summary: row["output_summary"] ?? null,
|
|
@@ -22853,13 +25003,65 @@ function verificationRowToRecord(row) {
|
|
|
22853
25003
|
}
|
|
22854
25004
|
};
|
|
22855
25005
|
}
|
|
25006
|
+
function portableVerificationRowToRecord(row) {
|
|
25007
|
+
let evidence = {};
|
|
25008
|
+
try {
|
|
25009
|
+
evidence = row["evidence"] ? JSON.parse(String(row["evidence"])) : {};
|
|
25010
|
+
} catch {
|
|
25011
|
+
evidence = {};
|
|
25012
|
+
}
|
|
25013
|
+
return {
|
|
25014
|
+
id: String(row["id"]),
|
|
25015
|
+
task_id: row["task_id"] ?? null,
|
|
25016
|
+
command: String(row["provider_name"] ?? "manual"),
|
|
25017
|
+
status: row["status"] ?? "unknown",
|
|
25018
|
+
output_summary: row["summary"] ?? null,
|
|
25019
|
+
artifact_path: row["artifact_id"] ?? null,
|
|
25020
|
+
agent_id: evidence["agent_id"] ?? null,
|
|
25021
|
+
run_at: String(row["completed_at"] ?? row["started_at"] ?? row["created_at"] ?? ""),
|
|
25022
|
+
created_at: String(row["created_at"] ?? ""),
|
|
25023
|
+
evidence: {
|
|
25024
|
+
...evidence,
|
|
25025
|
+
provider_name: row["provider_name"] ?? evidence["provider_name"] ?? null,
|
|
25026
|
+
provider_type: row["provider_type"] ?? evidence["provider_type"] ?? "local",
|
|
25027
|
+
status: row["status"] ?? evidence["status"] ?? "unknown",
|
|
25028
|
+
summary: row["summary"] ?? evidence["summary"] ?? null,
|
|
25029
|
+
artifact_id: row["artifact_id"] ?? evidence["artifact_id"] ?? null
|
|
25030
|
+
}
|
|
25031
|
+
};
|
|
25032
|
+
}
|
|
22856
25033
|
function getVerificationRecord(id, db) {
|
|
22857
25034
|
const d = db || getDatabase();
|
|
25035
|
+
try {
|
|
25036
|
+
const portable = d.query("SELECT * FROM verification_records WHERE id = ?").get(id);
|
|
25037
|
+
if (portable)
|
|
25038
|
+
return portableVerificationRowToRecord(portable);
|
|
25039
|
+
} catch {}
|
|
22858
25040
|
const row = d.query("SELECT * FROM task_verifications WHERE id = ?").get(id);
|
|
22859
|
-
return row ?
|
|
25041
|
+
return row ? taskVerificationRowToRecord(row) : null;
|
|
22860
25042
|
}
|
|
22861
25043
|
function listVerificationRecords(filter = {}, db) {
|
|
22862
25044
|
const d = db || getDatabase();
|
|
25045
|
+
const records = [];
|
|
25046
|
+
try {
|
|
25047
|
+
const conditions2 = [];
|
|
25048
|
+
const params2 = [];
|
|
25049
|
+
if (filter.task_id) {
|
|
25050
|
+
conditions2.push("task_id = ?");
|
|
25051
|
+
params2.push(filter.task_id);
|
|
25052
|
+
}
|
|
25053
|
+
if (filter.status) {
|
|
25054
|
+
conditions2.push("status = ?");
|
|
25055
|
+
params2.push(filter.status);
|
|
25056
|
+
}
|
|
25057
|
+
if (filter.provider) {
|
|
25058
|
+
conditions2.push("provider_name = ?");
|
|
25059
|
+
params2.push(filter.provider);
|
|
25060
|
+
}
|
|
25061
|
+
const where2 = conditions2.length > 0 ? `WHERE ${conditions2.join(" AND ")}` : "";
|
|
25062
|
+
const rows2 = d.query(`SELECT * FROM verification_records ${where2} ORDER BY COALESCE(completed_at, started_at, created_at) DESC, created_at DESC`).all(...params2);
|
|
25063
|
+
records.push(...rows2.map(portableVerificationRowToRecord));
|
|
25064
|
+
} catch {}
|
|
22863
25065
|
const conditions = [];
|
|
22864
25066
|
const params = [];
|
|
22865
25067
|
if (filter.task_id) {
|
|
@@ -22875,9 +25077,21 @@ function listVerificationRecords(filter = {}, db) {
|
|
|
22875
25077
|
params.push(filter.status);
|
|
22876
25078
|
}
|
|
22877
25079
|
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
22878
|
-
const
|
|
22879
|
-
|
|
22880
|
-
|
|
25080
|
+
const rows = d.query(`SELECT * FROM task_verifications ${where} ORDER BY run_at DESC, created_at DESC`).all(...params);
|
|
25081
|
+
records.push(...rows.map(taskVerificationRowToRecord));
|
|
25082
|
+
let filtered = records;
|
|
25083
|
+
if (filter.agent_id) {
|
|
25084
|
+
filtered = filtered.filter((record) => record.agent_id === filter.agent_id || record.evidence.agent_id === filter.agent_id);
|
|
25085
|
+
}
|
|
25086
|
+
if (filter.run_record_id) {
|
|
25087
|
+
filtered = filtered.filter((record) => record.evidence.run_record_id === filter.run_record_id);
|
|
25088
|
+
}
|
|
25089
|
+
filtered.sort((a, b) => {
|
|
25090
|
+
const at = Date.parse(a.run_at || a.created_at || "");
|
|
25091
|
+
const bt = Date.parse(b.run_at || b.created_at || "");
|
|
25092
|
+
return (Number.isFinite(bt) ? bt : 0) - (Number.isFinite(at) ? at : 0);
|
|
25093
|
+
});
|
|
25094
|
+
return filter.limit && filter.limit > 0 ? filtered.slice(0, Math.floor(filter.limit)) : filtered;
|
|
22881
25095
|
}
|
|
22882
25096
|
// src/lib/verification-evidence.ts
|
|
22883
25097
|
init_database();
|
|
@@ -22915,24 +25129,24 @@ function toPortableEvidence(record) {
|
|
|
22915
25129
|
id: record.id,
|
|
22916
25130
|
task_id: record.task_id,
|
|
22917
25131
|
run_record_id: ev.run_record_id ?? null,
|
|
22918
|
-
agent_id: ev.agent_id ?? verifier.agent_id ?? null,
|
|
22919
|
-
provider_name: record.
|
|
22920
|
-
provider_type:
|
|
25132
|
+
agent_id: record.agent_id ?? ev.agent_id ?? verifier.agent_id ?? null,
|
|
25133
|
+
provider_name: ev.provider_name ?? ev.provider ?? record.command,
|
|
25134
|
+
provider_type: ev.provider_type ?? "local",
|
|
22921
25135
|
status: record.status,
|
|
22922
|
-
summary: record.
|
|
25136
|
+
summary: ev.summary ?? record.output_summary ?? record.command,
|
|
22923
25137
|
confidence: typeof ev.confidence === "number" ? ev.confidence : null,
|
|
22924
25138
|
commands: ev.commands ?? [],
|
|
22925
25139
|
test_results: ev.test_results ?? [],
|
|
22926
25140
|
links: ev.links ?? [],
|
|
22927
25141
|
artifact_ids: [
|
|
22928
25142
|
...ev.artifact_ids ?? [],
|
|
22929
|
-
...record.
|
|
25143
|
+
...record.artifact_path ? [record.artifact_path] : []
|
|
22930
25144
|
],
|
|
22931
25145
|
log_excerpt: ev.log_excerpt ?? ev.stdout?.slice(-2000) ?? null,
|
|
22932
25146
|
screenshot_paths: ev.screenshot_paths ?? [],
|
|
22933
25147
|
verifier,
|
|
22934
|
-
started_at: record.
|
|
22935
|
-
completed_at: record.
|
|
25148
|
+
started_at: record.run_at,
|
|
25149
|
+
completed_at: record.run_at,
|
|
22936
25150
|
created_at: record.created_at,
|
|
22937
25151
|
metadata: ev.metadata ?? {}
|
|
22938
25152
|
};
|
|
@@ -23316,7 +25530,10 @@ function buildResourceSnapshot(uri, staleMs = DEFAULT_STALE_MS) {
|
|
|
23316
25530
|
};
|
|
23317
25531
|
}
|
|
23318
25532
|
function isSnapshotStale(snapshot, now3 = new Date) {
|
|
23319
|
-
|
|
25533
|
+
const staleAt = Date.parse(snapshot.stale_after);
|
|
25534
|
+
if (!Number.isFinite(staleAt))
|
|
25535
|
+
return true;
|
|
25536
|
+
return now3.getTime() >= staleAt;
|
|
23320
25537
|
}
|
|
23321
25538
|
function subscribeResource(uri, agentId) {
|
|
23322
25539
|
const key = `${uri}:${agentId || "*"}`;
|
|
@@ -23655,7 +25872,7 @@ async function runNextAgentDispatch(input = {}, db) {
|
|
|
23655
25872
|
return { run_id: next.run.id, task_id: next.run.task_id, command, dry_run: false, status, exit_code: exitCode, output_summary: outputSummary };
|
|
23656
25873
|
}
|
|
23657
25874
|
function listAgentRuns(filter = {}, db) {
|
|
23658
|
-
let runs = listAgentRunQueue(db);
|
|
25875
|
+
let runs = listAgentRunQueue(db).map(toAgentRun);
|
|
23659
25876
|
if (filter.task_id)
|
|
23660
25877
|
runs = runs.filter((r) => r.task_id === filter.task_id);
|
|
23661
25878
|
if (filter.status)
|
|
@@ -23667,11 +25884,12 @@ function listAgentRuns(filter = {}, db) {
|
|
|
23667
25884
|
return runs;
|
|
23668
25885
|
}
|
|
23669
25886
|
function retryAgentRun(runId, db) {
|
|
23670
|
-
return retryAgentRunDispatch(runId, db);
|
|
25887
|
+
return toAgentRun(retryAgentRunDispatch(runId, db));
|
|
23671
25888
|
}
|
|
23672
25889
|
var AGENT_RUN_SCHEMA_VERSION = "todos.agent_run.v1";
|
|
23673
25890
|
function toAgentRun(item) {
|
|
23674
25891
|
const md = item.run.metadata || {};
|
|
25892
|
+
const maxRetries = typeof md["agent_run_max_retries"] === "number" ? md["agent_run_max_retries"] : 3;
|
|
23675
25893
|
return {
|
|
23676
25894
|
id: item.run.id,
|
|
23677
25895
|
task_id: item.run.task_id,
|
|
@@ -23679,7 +25897,10 @@ function toAgentRun(item) {
|
|
|
23679
25897
|
adapter: item.dispatcher.adapter ?? null,
|
|
23680
25898
|
status: item.dispatcher.state,
|
|
23681
25899
|
command: item.dispatcher.command ?? null,
|
|
25900
|
+
error: item.dispatcher.last_error ?? null,
|
|
23682
25901
|
evidence: md["agent_run_evidence"] ?? {},
|
|
25902
|
+
retry_count: Math.max(0, item.dispatcher.attempt - 1),
|
|
25903
|
+
max_retries: maxRetries,
|
|
23683
25904
|
queued_at: item.dispatcher.queued_at,
|
|
23684
25905
|
started_at: item.dispatcher.started_at ?? null,
|
|
23685
25906
|
completed_at: item.dispatcher.completed_at ?? null,
|
|
@@ -24016,11 +26237,11 @@ function looksLikeFile(value) {
|
|
|
24016
26237
|
}
|
|
24017
26238
|
function parseMention(raw) {
|
|
24018
26239
|
const input = raw.trim();
|
|
24019
|
-
const
|
|
24020
|
-
if (/^#\d+$/.test(
|
|
24021
|
-
return { input, kind: "pull_request", target:
|
|
26240
|
+
const clean3 = input.startsWith("@") ? input.slice(1) : input;
|
|
26241
|
+
if (/^#\d+$/.test(clean3)) {
|
|
26242
|
+
return { input, kind: "pull_request", target: clean3.slice(1), explicit: true };
|
|
24022
26243
|
}
|
|
24023
|
-
const prefixMatch =
|
|
26244
|
+
const prefixMatch = clean3.match(/^([a-z_]+):(.*)$/i);
|
|
24024
26245
|
if (prefixMatch && PREFIXES[prefixMatch[1].toLowerCase()]) {
|
|
24025
26246
|
const kind = PREFIXES[prefixMatch[1].toLowerCase()];
|
|
24026
26247
|
const target = prefixMatch[2].trim();
|
|
@@ -24030,13 +26251,13 @@ function parseMention(raw) {
|
|
|
24030
26251
|
}
|
|
24031
26252
|
return { input, kind, target, explicit: true };
|
|
24032
26253
|
}
|
|
24033
|
-
if (looksLikeCommit(
|
|
24034
|
-
return { input, kind: "commit", target:
|
|
24035
|
-
if (looksLikeFile(
|
|
24036
|
-
const parsed = parseLineAnchor(
|
|
26254
|
+
if (looksLikeCommit(clean3))
|
|
26255
|
+
return { input, kind: "commit", target: clean3, explicit: false };
|
|
26256
|
+
if (looksLikeFile(clean3)) {
|
|
26257
|
+
const parsed = parseLineAnchor(clean3);
|
|
24037
26258
|
return { input, kind: "file", target: parsed.path, line: parsed.line, explicit: false };
|
|
24038
26259
|
}
|
|
24039
|
-
return { input, kind: "unknown", target:
|
|
26260
|
+
return { input, kind: "unknown", target: clean3, explicit: false };
|
|
24040
26261
|
}
|
|
24041
26262
|
function runGit2(root, args) {
|
|
24042
26263
|
try {
|
|
@@ -24090,20 +26311,20 @@ function resolveFile(parsed, workspace) {
|
|
|
24090
26311
|
function walkSourceFiles(root, current = root, files = []) {
|
|
24091
26312
|
if (files.length >= 5000)
|
|
24092
26313
|
return files;
|
|
24093
|
-
for (const
|
|
24094
|
-
if (
|
|
24095
|
-
if (SKIP_DIRS.has(
|
|
26314
|
+
for (const entry2 of readdirSync2(current, { withFileTypes: true })) {
|
|
26315
|
+
if (entry2.name.startsWith(".") && ![".github"].includes(entry2.name)) {
|
|
26316
|
+
if (SKIP_DIRS.has(entry2.name))
|
|
24096
26317
|
continue;
|
|
24097
26318
|
}
|
|
24098
|
-
const absolutePath = join11(current,
|
|
24099
|
-
if (
|
|
24100
|
-
if (!SKIP_DIRS.has(
|
|
26319
|
+
const absolutePath = join11(current, entry2.name);
|
|
26320
|
+
if (entry2.isDirectory()) {
|
|
26321
|
+
if (!SKIP_DIRS.has(entry2.name))
|
|
24101
26322
|
walkSourceFiles(root, absolutePath, files);
|
|
24102
26323
|
continue;
|
|
24103
26324
|
}
|
|
24104
|
-
if (!
|
|
26325
|
+
if (!entry2.isFile())
|
|
24105
26326
|
continue;
|
|
24106
|
-
const extension = `.${basename3(
|
|
26327
|
+
const extension = `.${basename3(entry2.name).split(".").pop() || ""}`;
|
|
24107
26328
|
if (SOURCE_EXTENSIONS.has(extension) && statSync4(absolutePath).size <= 512 * 1024) {
|
|
24108
26329
|
files.push(absolutePath);
|
|
24109
26330
|
}
|
|
@@ -24420,7 +26641,7 @@ function getTaskLabels(taskId, db) {
|
|
|
24420
26641
|
// src/db/custom-fields.ts
|
|
24421
26642
|
init_database();
|
|
24422
26643
|
var CUSTOM_FIELD_TYPES = ["text", "number", "boolean", "date", "enum"];
|
|
24423
|
-
function
|
|
26644
|
+
function slugify3(name) {
|
|
24424
26645
|
return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
24425
26646
|
}
|
|
24426
26647
|
function rowToDef(row) {
|
|
@@ -24441,7 +26662,7 @@ function createCustomFieldDefinition(input, db) {
|
|
|
24441
26662
|
const d = db || getDatabase();
|
|
24442
26663
|
const id = uuid();
|
|
24443
26664
|
const ts = now();
|
|
24444
|
-
const slug =
|
|
26665
|
+
const slug = slugify3(input.name);
|
|
24445
26666
|
d.run(`INSERT INTO custom_field_definitions (
|
|
24446
26667
|
id, project_id, name, slug, field_type, options, required, default_value, sort_order, created_at, updated_at
|
|
24447
26668
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
@@ -24463,7 +26684,7 @@ function getCustomFieldDefinition(idOrSlug, db) {
|
|
|
24463
26684
|
const d = db || getDatabase();
|
|
24464
26685
|
let row = d.query("SELECT * FROM custom_field_definitions WHERE id = ?").get(idOrSlug);
|
|
24465
26686
|
if (!row)
|
|
24466
|
-
row = d.query("SELECT * FROM custom_field_definitions WHERE slug = ?").get(
|
|
26687
|
+
row = d.query("SELECT * FROM custom_field_definitions WHERE slug = ?").get(slugify3(idOrSlug));
|
|
24467
26688
|
return row ? rowToDef(row) : null;
|
|
24468
26689
|
}
|
|
24469
26690
|
function listCustomFieldDefinitions(projectId, db) {
|
|
@@ -25157,11 +27378,10 @@ function acquireTaskLease(taskId, agentId, ttlMinutes = DEFAULT_LEASE_MINUTES, d
|
|
|
25157
27378
|
const d = db || getDatabase();
|
|
25158
27379
|
const lock = lockTask(taskId, agentId, d);
|
|
25159
27380
|
if (!lock.success) {
|
|
25160
|
-
const task2 = getTask(taskId, d);
|
|
25161
27381
|
const existing = d.query("SELECT * FROM task_leases WHERE task_id = ?").get(taskId);
|
|
25162
27382
|
return {
|
|
25163
27383
|
success: false,
|
|
25164
|
-
conflict: formatLockConflict(taskId, lock.locked_by, lock.locked_at, existing?.expires_at ?? null)
|
|
27384
|
+
conflict: formatLockConflict(taskId, lock.locked_by ?? "unknown", lock.locked_at ?? null, existing?.expires_at ?? null)
|
|
25165
27385
|
};
|
|
25166
27386
|
}
|
|
25167
27387
|
const ts = now();
|
|
@@ -25230,7 +27450,7 @@ function stealTaskLease(taskId, agentId, options = {}, db) {
|
|
|
25230
27450
|
if (stolen?.id === taskId)
|
|
25231
27451
|
acquired = true;
|
|
25232
27452
|
else
|
|
25233
|
-
return { success: false, conflict: formatLockConflict(taskId, lock.locked_by, lock.locked_at, lease?.expires_at ?? null) };
|
|
27453
|
+
return { success: false, conflict: formatLockConflict(taskId, lock.locked_by ?? "unknown", lock.locked_at ?? null, lease?.expires_at ?? null) };
|
|
25234
27454
|
}
|
|
25235
27455
|
if (!acquired) {
|
|
25236
27456
|
return { success: false, conflict: formatLockConflict(taskId, previous ?? "unknown", task2.locked_at) };
|
|
@@ -25375,19 +27595,19 @@ function getParityReport() {
|
|
|
25375
27595
|
let cliOnly = 0;
|
|
25376
27596
|
let mcpOnly = 0;
|
|
25377
27597
|
let documentedGaps = 0;
|
|
25378
|
-
for (const
|
|
25379
|
-
byDomain[
|
|
25380
|
-
if (
|
|
27598
|
+
for (const entry2 of CLI_MCP_PARITY_MANIFEST) {
|
|
27599
|
+
byDomain[entry2.domain] = byDomain[entry2.domain] ?? { matched: 0, gaps: 0 };
|
|
27600
|
+
if (entry2.cli && entry2.mcp) {
|
|
25381
27601
|
matched++;
|
|
25382
|
-
byDomain[
|
|
25383
|
-
} else if (
|
|
27602
|
+
byDomain[entry2.domain].matched++;
|
|
27603
|
+
} else if (entry2.cli && !entry2.mcp) {
|
|
25384
27604
|
cliOnly++;
|
|
25385
27605
|
documentedGaps++;
|
|
25386
|
-
byDomain[
|
|
25387
|
-
} else if (!
|
|
27606
|
+
byDomain[entry2.domain].gaps++;
|
|
27607
|
+
} else if (!entry2.cli && entry2.mcp) {
|
|
25388
27608
|
mcpOnly++;
|
|
25389
27609
|
}
|
|
25390
|
-
if (
|
|
27610
|
+
if (entry2.gap)
|
|
25391
27611
|
documentedGaps++;
|
|
25392
27612
|
}
|
|
25393
27613
|
return {
|
|
@@ -25404,16 +27624,16 @@ function getParityReport() {
|
|
|
25404
27624
|
function validateParityManifest() {
|
|
25405
27625
|
const issues = [];
|
|
25406
27626
|
const seen = new Set;
|
|
25407
|
-
for (const
|
|
25408
|
-
const key = `${
|
|
27627
|
+
for (const entry2 of CLI_MCP_PARITY_MANIFEST) {
|
|
27628
|
+
const key = `${entry2.domain}:${entry2.operation}`;
|
|
25409
27629
|
if (seen.has(key))
|
|
25410
27630
|
issues.push(`Duplicate parity entry: ${key}`);
|
|
25411
27631
|
seen.add(key);
|
|
25412
|
-
if (!
|
|
27632
|
+
if (!entry2.cli && !entry2.mcp && !entry2.gap) {
|
|
25413
27633
|
issues.push(`Entry ${key} has no cli, mcp, or gap documentation`);
|
|
25414
27634
|
}
|
|
25415
|
-
if (
|
|
25416
|
-
if (!
|
|
27635
|
+
if (entry2.cli && !entry2.mcp && !entry2.gap || !entry2.cli && entry2.mcp && !entry2.gap) {
|
|
27636
|
+
if (!entry2.notes) {
|
|
25417
27637
|
issues.push(`Entry ${key} is one-sided without gap or notes`);
|
|
25418
27638
|
}
|
|
25419
27639
|
}
|
|
@@ -25471,7 +27691,7 @@ var PROFILE_META = {
|
|
|
25471
27691
|
},
|
|
25472
27692
|
admin: {
|
|
25473
27693
|
name: "admin",
|
|
25474
|
-
description: "Full access including migrations and
|
|
27694
|
+
description: "Full access including migrations and storage bridge tools",
|
|
25475
27695
|
surfaces: ["cli", "mcp", "sdk", "http"],
|
|
25476
27696
|
allows_mutations: true,
|
|
25477
27697
|
allows_admin: true
|
|
@@ -25694,7 +27914,6 @@ var AGENT_ADAPTER_DOCS = {
|
|
|
25694
27914
|
display_name: "OpenAI Codex CLI",
|
|
25695
27915
|
install: {
|
|
25696
27916
|
bun: "bun install -g @hasna/todos",
|
|
25697
|
-
npm: "npm install -g @hasna/todos",
|
|
25698
27917
|
verify: "todos --version && which todos-mcp"
|
|
25699
27918
|
},
|
|
25700
27919
|
mcp: {
|
|
@@ -25972,7 +28191,7 @@ function getAdapterDocsFingerprint() {
|
|
|
25972
28191
|
init_database();
|
|
25973
28192
|
import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
|
|
25974
28193
|
import { basename as basename4 } from "path";
|
|
25975
|
-
import { createHash as
|
|
28194
|
+
import { createHash as createHash10 } from "crypto";
|
|
25976
28195
|
init_secret_redaction();
|
|
25977
28196
|
var INBOX_INTAKE_SCHEMA = "todos.inbox_intake.v1";
|
|
25978
28197
|
var INTAKE_SOURCE_TYPES = [
|
|
@@ -25985,7 +28204,7 @@ var INTAKE_SOURCE_TYPES = [
|
|
|
25985
28204
|
];
|
|
25986
28205
|
var INTAKE_TRIAGE_STATUSES = ["preview", "triaged", "duplicate", "created"];
|
|
25987
28206
|
function fingerprint2(text) {
|
|
25988
|
-
return
|
|
28207
|
+
return createHash10("sha256").update(text).digest("hex").slice(0, 16);
|
|
25989
28208
|
}
|
|
25990
28209
|
function loadRawContent(input) {
|
|
25991
28210
|
if (input.github_url) {
|
|
@@ -26136,7 +28355,7 @@ ${parsed.failures.map((f) => `- ${f}`).join(`
|
|
|
26136
28355
|
return {
|
|
26137
28356
|
title: titleOverride ?? raw.split(/\n/)[0].slice(0, 200),
|
|
26138
28357
|
description: raw.includes(`
|
|
26139
|
-
`) ? raw.slice(0, 8000) :
|
|
28358
|
+
`) ? raw.slice(0, 8000) : "",
|
|
26140
28359
|
extra: {}
|
|
26141
28360
|
};
|
|
26142
28361
|
}
|
|
@@ -26153,10 +28372,9 @@ function findIntakeDuplicate(createInput, sourceFingerprint, db) {
|
|
|
26153
28372
|
return { task_id: t.id, short_id: t.short_id, title: t.title, score: 1 };
|
|
26154
28373
|
}
|
|
26155
28374
|
}
|
|
26156
|
-
const candidates = findDuplicateCandidates({
|
|
28375
|
+
const candidates = findDuplicateCandidates({ threshold: 0.75, limit: 5 }, db);
|
|
26157
28376
|
for (const c of candidates) {
|
|
26158
|
-
const
|
|
26159
|
-
const other = getTask(otherId, db);
|
|
28377
|
+
const other = c.primary_task;
|
|
26160
28378
|
if (!other)
|
|
26161
28379
|
continue;
|
|
26162
28380
|
const normNew = createInput.title.toLowerCase();
|
|
@@ -26655,7 +28873,7 @@ var JIRA_PRIORITY = {
|
|
|
26655
28873
|
low: "low",
|
|
26656
28874
|
lowest: "low"
|
|
26657
28875
|
};
|
|
26658
|
-
function
|
|
28876
|
+
function asRecord3(value) {
|
|
26659
28877
|
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
26660
28878
|
}
|
|
26661
28879
|
function labelNames2(labels) {
|
|
@@ -26664,7 +28882,7 @@ function labelNames2(labels) {
|
|
|
26664
28882
|
return labels.map((label) => {
|
|
26665
28883
|
if (typeof label === "string")
|
|
26666
28884
|
return label;
|
|
26667
|
-
const rec =
|
|
28885
|
+
const rec = asRecord3(label);
|
|
26668
28886
|
return typeof rec?.name === "string" ? rec.name : null;
|
|
26669
28887
|
}).filter((name) => !!name);
|
|
26670
28888
|
}
|
|
@@ -26690,7 +28908,7 @@ function priorityFromLinear(value) {
|
|
|
26690
28908
|
return "medium";
|
|
26691
28909
|
}
|
|
26692
28910
|
function priorityFromJira(value) {
|
|
26693
|
-
const rec =
|
|
28911
|
+
const rec = asRecord3(value);
|
|
26694
28912
|
const name = typeof rec?.name === "string" ? rec.name.toLowerCase() : "";
|
|
26695
28913
|
return JIRA_PRIORITY[name] ?? "medium";
|
|
26696
28914
|
}
|
|
@@ -26732,10 +28950,10 @@ function normalizeLinearIssue(raw) {
|
|
|
26732
28950
|
const title = typeof raw.title === "string" ? raw.title.trim() : "";
|
|
26733
28951
|
if (!identifier || !title)
|
|
26734
28952
|
return null;
|
|
26735
|
-
const stateRec =
|
|
28953
|
+
const stateRec = asRecord3(raw.state);
|
|
26736
28954
|
const state = typeof raw.state === "string" ? raw.state : typeof stateRec?.name === "string" ? stateRec.name : undefined;
|
|
26737
28955
|
const url = typeof raw.url === "string" ? raw.url : undefined;
|
|
26738
|
-
const labelSource =
|
|
28956
|
+
const labelSource = asRecord3(raw.labels);
|
|
26739
28957
|
const labels = labelNames2(Array.isArray(labelSource?.nodes) ? labelSource.nodes : raw.labels);
|
|
26740
28958
|
return {
|
|
26741
28959
|
source: "linear",
|
|
@@ -26758,16 +28976,16 @@ function normalizeLinearIssue(raw) {
|
|
|
26758
28976
|
}
|
|
26759
28977
|
function normalizeJiraIssue(raw) {
|
|
26760
28978
|
const key = typeof raw.key === "string" ? raw.key.trim() : "";
|
|
26761
|
-
const fields =
|
|
28979
|
+
const fields = asRecord3(raw.fields);
|
|
26762
28980
|
if (!key || !fields)
|
|
26763
28981
|
return null;
|
|
26764
28982
|
const summary = typeof fields.summary === "string" ? fields.summary.trim() : "";
|
|
26765
28983
|
if (!summary)
|
|
26766
28984
|
return null;
|
|
26767
|
-
const statusRec =
|
|
28985
|
+
const statusRec = asRecord3(fields.status);
|
|
26768
28986
|
const status = typeof statusRec?.name === "string" ? statusRec.name : undefined;
|
|
26769
28987
|
const labels = labelNames2(fields.labels);
|
|
26770
|
-
const description = typeof fields.description === "string" ? fields.description.slice(0, 8000) : typeof
|
|
28988
|
+
const description = typeof fields.description === "string" ? fields.description.slice(0, 8000) : typeof asRecord3(fields.description)?.content === "string" ? String(asRecord3(fields.description)?.content).slice(0, 8000) : undefined;
|
|
26771
28989
|
return {
|
|
26772
28990
|
source: "jira",
|
|
26773
28991
|
external_ref: `jira:${key}`,
|
|
@@ -26781,13 +28999,13 @@ function normalizeJiraIssue(raw) {
|
|
|
26781
28999
|
raw_metadata: {
|
|
26782
29000
|
jira_key: key,
|
|
26783
29001
|
jira_status: status,
|
|
26784
|
-
jira_priority:
|
|
29002
|
+
jira_priority: asRecord3(fields.priority)?.name,
|
|
26785
29003
|
jira_id: raw.id
|
|
26786
29004
|
}
|
|
26787
29005
|
};
|
|
26788
29006
|
}
|
|
26789
29007
|
function looksLikeJiraIssue(raw) {
|
|
26790
|
-
return typeof raw.key === "string" && !!
|
|
29008
|
+
return typeof raw.key === "string" && !!asRecord3(raw.fields)?.summary;
|
|
26791
29009
|
}
|
|
26792
29010
|
function looksLikeLinearIssue(raw) {
|
|
26793
29011
|
if (typeof raw.identifier !== "string")
|
|
@@ -26814,17 +29032,17 @@ function detectIssueExportSource(data) {
|
|
|
26814
29032
|
}
|
|
26815
29033
|
function extractIssueRecords(data) {
|
|
26816
29034
|
if (Array.isArray(data)) {
|
|
26817
|
-
return data.map(
|
|
29035
|
+
return data.map(asRecord3).filter((item) => !!item);
|
|
26818
29036
|
}
|
|
26819
|
-
const root =
|
|
29037
|
+
const root = asRecord3(data);
|
|
26820
29038
|
if (!root)
|
|
26821
29039
|
return [];
|
|
26822
|
-
const dataNode =
|
|
26823
|
-
const issuesNode =
|
|
26824
|
-
const nestedIssues =
|
|
29040
|
+
const dataNode = asRecord3(root.data);
|
|
29041
|
+
const issuesNode = asRecord3(root.issues);
|
|
29042
|
+
const nestedIssues = asRecord3(dataNode?.issues);
|
|
26825
29043
|
const nodes = nestedIssues?.nodes ?? issuesNode?.nodes ?? root.issues ?? dataNode?.issues;
|
|
26826
29044
|
if (Array.isArray(nodes)) {
|
|
26827
|
-
return nodes.map(
|
|
29045
|
+
return nodes.map(asRecord3).filter((item) => !!item);
|
|
26828
29046
|
}
|
|
26829
29047
|
if (looksLikeGitHubIssue(root) || looksLikeLinearIssue(root) || looksLikeJiraIssue(root)) {
|
|
26830
29048
|
return [root];
|
|
@@ -26880,9 +29098,9 @@ function findIssueDuplicate(createInput, db) {
|
|
|
26880
29098
|
return { task_id: task2.id, short_id: task2.short_id, title: task2.title, score: 1 };
|
|
26881
29099
|
}
|
|
26882
29100
|
}
|
|
26883
|
-
const candidates = findDuplicateCandidates({
|
|
29101
|
+
const candidates = findDuplicateCandidates({ threshold: 0.85, limit: 5 }, db);
|
|
26884
29102
|
for (const candidate of candidates) {
|
|
26885
|
-
const other =
|
|
29103
|
+
const other = candidate.primary_task;
|
|
26886
29104
|
if (!other)
|
|
26887
29105
|
continue;
|
|
26888
29106
|
if (other.title.toLowerCase() === createInput.title.toLowerCase()) {
|
|
@@ -27260,7 +29478,10 @@ import { existsSync as existsSync17, readFileSync as readFileSync15, readdirSync
|
|
|
27260
29478
|
import { join as join13, relative as relative5 } from "path";
|
|
27261
29479
|
var RELEASE_CHECK_SCHEMA = "todos.release_check.v1";
|
|
27262
29480
|
var FORBIDDEN_DIST_PATTERNS = [
|
|
27263
|
-
{
|
|
29481
|
+
{
|
|
29482
|
+
id: "hosted_platform_todos",
|
|
29483
|
+
pattern: new RegExp(`${["platform", "todos"].join("-")}|@${["hasna", "studio"].join("")}/${["platform", "todos"].join("-")}`, "i")
|
|
29484
|
+
},
|
|
27264
29485
|
{ id: "stripe", pattern: /\bstripe\.(?:com|js)\b|checkout\.sessions/i },
|
|
27265
29486
|
{ id: "hardcoded_aws_key", pattern: /\bAKIA[0-9A-Z]{16}\b/ },
|
|
27266
29487
|
{ id: "openai_key", pattern: /\bsk-[a-zA-Z0-9]{20,}\b/ }
|
|
@@ -27275,12 +29496,12 @@ function readPackageJson3(root) {
|
|
|
27275
29496
|
function walkFiles(dir, acc = []) {
|
|
27276
29497
|
if (!existsSync17(dir))
|
|
27277
29498
|
return acc;
|
|
27278
|
-
for (const
|
|
27279
|
-
const full = join13(dir,
|
|
29499
|
+
for (const entry2 of readdirSync3(dir)) {
|
|
29500
|
+
const full = join13(dir, entry2);
|
|
27280
29501
|
const st = statSync5(full);
|
|
27281
29502
|
if (st.isDirectory())
|
|
27282
29503
|
walkFiles(full, acc);
|
|
27283
|
-
else if (/\.(js|mjs|cjs|json|d\.ts)$/.test(
|
|
29504
|
+
else if (/\.(js|mjs|cjs|json|d\.ts)$/.test(entry2))
|
|
27284
29505
|
acc.push(full);
|
|
27285
29506
|
}
|
|
27286
29507
|
return acc;
|
|
@@ -27419,7 +29640,7 @@ todos-mcp --help 2>&1 | head -1 || true
|
|
|
27419
29640
|
- publishConfig public access
|
|
27420
29641
|
- prepublishOnly build hook
|
|
27421
29642
|
|
|
27422
|
-
|
|
29643
|
+
Remote sync is explicit opt-in \u2014 not required for local-only usage.
|
|
27423
29644
|
`;
|
|
27424
29645
|
}
|
|
27425
29646
|
function runReleaseChecks(options = {}) {
|
|
@@ -27656,12 +29877,13 @@ function backupDatabase(outputPath, sourcePath) {
|
|
|
27656
29877
|
throw new Error(`Database not found: ${source9}`);
|
|
27657
29878
|
mkdirSync12(dirname11(outputPath), { recursive: true });
|
|
27658
29879
|
closeDatabase();
|
|
27659
|
-
const src = new Database3(source9
|
|
29880
|
+
const src = new Database3(source9);
|
|
27660
29881
|
try {
|
|
27661
|
-
src.exec("PRAGMA wal_checkpoint(
|
|
29882
|
+
src.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
27662
29883
|
} catch {}
|
|
29884
|
+
const image = src.serialize();
|
|
27663
29885
|
src.close();
|
|
27664
|
-
|
|
29886
|
+
writeFileSync10(outputPath, image);
|
|
27665
29887
|
const method = "file_copy";
|
|
27666
29888
|
const bytes = statSync6(outputPath).size;
|
|
27667
29889
|
return {
|
|
@@ -27980,8 +30202,8 @@ var SCHEMA_CONTRACT_FIXTURES = {
|
|
|
27980
30202
|
project: {
|
|
27981
30203
|
schema_version: "todos.project.v1",
|
|
27982
30204
|
id: "proj-001",
|
|
27983
|
-
name: "
|
|
27984
|
-
path: "/tmp/
|
|
30205
|
+
name: "todos-example",
|
|
30206
|
+
path: "/tmp/todos-example",
|
|
27985
30207
|
task_counter: 0,
|
|
27986
30208
|
created_at: "2026-01-01T00:00:00.000Z",
|
|
27987
30209
|
updated_at: "2026-01-01T00:00:00.000Z"
|
|
@@ -28286,7 +30508,7 @@ function listReadyScheduledTasks(filters = {}, db) {
|
|
|
28286
30508
|
return !start || start <= ts;
|
|
28287
30509
|
});
|
|
28288
30510
|
}
|
|
28289
|
-
function getAgentSafeQueue(
|
|
30511
|
+
function getAgentSafeQueue(_agentId, filters = {}, db) {
|
|
28290
30512
|
const d = db || getDatabase();
|
|
28291
30513
|
const ts = Date.now();
|
|
28292
30514
|
const ready = listReadyScheduledTasks({ project_id: filters.project_id }, d);
|
|
@@ -28507,7 +30729,7 @@ function searchTasks(options, projectId, taskListId, db) {
|
|
|
28507
30729
|
|
|
28508
30730
|
// src/lib/saved-views.ts
|
|
28509
30731
|
var SAVED_VIEWS_SCHEMA = "todos.saved_views.v1";
|
|
28510
|
-
function
|
|
30732
|
+
function slugify4(name) {
|
|
28511
30733
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
28512
30734
|
}
|
|
28513
30735
|
function rowToView(row) {
|
|
@@ -28526,7 +30748,7 @@ function createSavedView(input, db) {
|
|
|
28526
30748
|
const d = db || getDatabase();
|
|
28527
30749
|
const id = uuid();
|
|
28528
30750
|
const ts = now();
|
|
28529
|
-
const slug = input.slug ??
|
|
30751
|
+
const slug = input.slug ?? slugify4(input.name);
|
|
28530
30752
|
d.run(`INSERT INTO saved_views (id, name, slug, entity_type, filters, created_at, updated_at)
|
|
28531
30753
|
VALUES (?, ?, ?, ?, ?, ?, ?)`, [id, input.name, slug, input.entity_type ?? "task", JSON.stringify(input.filters ?? {}), ts, ts]);
|
|
28532
30754
|
return getSavedView(id, d);
|
|
@@ -28604,12 +30826,13 @@ function unifiedSearch(input = {}, db) {
|
|
|
28604
30826
|
}
|
|
28605
30827
|
if (types.includes("run") || types.includes("all")) {
|
|
28606
30828
|
for (const r of listAgentRuns({ limit: 100 }, d)) {
|
|
28607
|
-
|
|
30829
|
+
const adapter = r.adapter ?? "custom";
|
|
30830
|
+
if (query && !adapter.toLowerCase().includes(query.toLowerCase()) && !r.id.includes(query))
|
|
28608
30831
|
continue;
|
|
28609
30832
|
hits.push({
|
|
28610
30833
|
entity_type: "run",
|
|
28611
30834
|
id: r.id,
|
|
28612
|
-
title: `Run ${
|
|
30835
|
+
title: `Run ${adapter}`,
|
|
28613
30836
|
snippet: r.status,
|
|
28614
30837
|
score: 60,
|
|
28615
30838
|
data: { status: r.status, task_id: r.task_id }
|
|
@@ -29106,6 +31329,15 @@ init_secret_redaction();
|
|
|
29106
31329
|
var BUNDLE_SCHEMA = "todos.bundle.v1";
|
|
29107
31330
|
var BUNDLE_TYPES = ["full_export", "tasks", "partial"];
|
|
29108
31331
|
var MERGE_STRATEGIES = ["skip_existing", "remote_wins", "local_wins", "newest_wins"];
|
|
31332
|
+
function sqlValue(value) {
|
|
31333
|
+
if (value === undefined || value === null)
|
|
31334
|
+
return null;
|
|
31335
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean")
|
|
31336
|
+
return value;
|
|
31337
|
+
if (value instanceof Uint8Array)
|
|
31338
|
+
return value;
|
|
31339
|
+
return JSON.stringify(value);
|
|
31340
|
+
}
|
|
29109
31341
|
function serializeProject(project) {
|
|
29110
31342
|
return {
|
|
29111
31343
|
schema_version: "todos.project.v1",
|
|
@@ -29202,10 +31434,11 @@ function exportLocalBundle(options = {}, db) {
|
|
|
29202
31434
|
verification_records: verificationRecords,
|
|
29203
31435
|
artifacts
|
|
29204
31436
|
};
|
|
29205
|
-
const { data
|
|
31437
|
+
const { data, warnings: profileWarnings } = applyExportProfile(bundleData, {
|
|
29206
31438
|
profile,
|
|
29207
31439
|
acknowledge_plaintext: options.acknowledge_plaintext
|
|
29208
31440
|
});
|
|
31441
|
+
const redacted = data;
|
|
29209
31442
|
warnings.push(...profileWarnings);
|
|
29210
31443
|
return {
|
|
29211
31444
|
schema_version: BUNDLE_SCHEMA,
|
|
@@ -29350,7 +31583,7 @@ function upsertProject(raw, d) {
|
|
|
29350
31583
|
raw.task_counter ?? 0,
|
|
29351
31584
|
raw.created_at ?? ts,
|
|
29352
31585
|
raw.updated_at ?? ts
|
|
29353
|
-
]);
|
|
31586
|
+
].map(sqlValue));
|
|
29354
31587
|
return "created";
|
|
29355
31588
|
}
|
|
29356
31589
|
d.run(`UPDATE projects SET name = ?, path = ?, description = ?, task_list_id = ?, task_prefix = ?, task_counter = ?, updated_at = ?
|
|
@@ -29363,7 +31596,7 @@ function upsertProject(raw, d) {
|
|
|
29363
31596
|
raw.task_counter ?? existing.task_counter,
|
|
29364
31597
|
raw.updated_at ?? ts,
|
|
29365
31598
|
id
|
|
29366
|
-
]);
|
|
31599
|
+
].map(sqlValue));
|
|
29367
31600
|
return "updated";
|
|
29368
31601
|
}
|
|
29369
31602
|
function upsertTask(raw, d) {
|
|
@@ -29399,7 +31632,7 @@ function upsertTask(raw, d) {
|
|
|
29399
31632
|
raw.version ?? existing.version,
|
|
29400
31633
|
raw.updated_at ?? now(),
|
|
29401
31634
|
id
|
|
29402
|
-
]);
|
|
31635
|
+
].map(sqlValue));
|
|
29403
31636
|
return "updated";
|
|
29404
31637
|
}
|
|
29405
31638
|
function upsertComment(raw, d) {
|
|
@@ -29417,7 +31650,7 @@ function upsertComment(raw, d) {
|
|
|
29417
31650
|
raw.type ?? "comment",
|
|
29418
31651
|
raw.progress_pct ?? null,
|
|
29419
31652
|
raw.created_at ?? now()
|
|
29420
|
-
]);
|
|
31653
|
+
].map(sqlValue));
|
|
29421
31654
|
return "created";
|
|
29422
31655
|
}
|
|
29423
31656
|
function importBundle(bundle, options = {}, db) {
|
|
@@ -29512,7 +31745,7 @@ function readBundleFile(path) {
|
|
|
29512
31745
|
function getBridgeDocs() {
|
|
29513
31746
|
return `# Local Import/Export/Sync Bridge
|
|
29514
31747
|
|
|
29515
|
-
OSS @hasna/todos produces stable \`${BUNDLE_SCHEMA}\` JSON bundles for
|
|
31748
|
+
OSS @hasna/todos produces stable \`${BUNDLE_SCHEMA}\` JSON bundles for hosted wrapper consumption.
|
|
29516
31749
|
No automatic cloud calls are made from the OSS package.
|
|
29517
31750
|
|
|
29518
31751
|
## Bundle types
|
|
@@ -29989,7 +32222,7 @@ function summarizeTask2(t) {
|
|
|
29989
32222
|
title: t.title,
|
|
29990
32223
|
status: t.status,
|
|
29991
32224
|
priority: t.priority,
|
|
29992
|
-
assigned_to: t.assigned_to
|
|
32225
|
+
assigned_to: t.assigned_to ?? null
|
|
29993
32226
|
};
|
|
29994
32227
|
}
|
|
29995
32228
|
function buildHandoffPacket(input = {}, db) {
|
|
@@ -30891,8 +33124,6 @@ var BUILTIN_TEMPLATES = [
|
|
|
30891
33124
|
},
|
|
30892
33125
|
{
|
|
30893
33126
|
name: "open-source-project",
|
|
30894
|
-
version: 1,
|
|
30895
|
-
category: "project",
|
|
30896
33127
|
description: "Full open-source project bootstrap \u2014 scaffold to publish",
|
|
30897
33128
|
category: "open-source",
|
|
30898
33129
|
version: BUILTIN_TEMPLATE_LIBRARY_VERSION,
|
|
@@ -30918,7 +33149,7 @@ var BUILTIN_TEMPLATES = [
|
|
|
30918
33149
|
},
|
|
30919
33150
|
{
|
|
30920
33151
|
name: "release",
|
|
30921
|
-
version:
|
|
33152
|
+
version: BUILTIN_TEMPLATE_LIBRARY_VERSION,
|
|
30922
33153
|
category: "ops",
|
|
30923
33154
|
description: "Version release workflow \u2014 test, changelog, publish, verify",
|
|
30924
33155
|
variables: [
|
|
@@ -30935,7 +33166,7 @@ var BUILTIN_TEMPLATES = [
|
|
|
30935
33166
|
},
|
|
30936
33167
|
{
|
|
30937
33168
|
name: "docs-refresh",
|
|
30938
|
-
version:
|
|
33169
|
+
version: BUILTIN_TEMPLATE_LIBRARY_VERSION,
|
|
30939
33170
|
category: "workflow",
|
|
30940
33171
|
description: "Documentation refresh \u2014 audit, update, verify links",
|
|
30941
33172
|
variables: [{ name: "scope", required: true, description: "Docs scope (README, API, AGENTS.md)" }],
|
|
@@ -30948,7 +33179,7 @@ var BUILTIN_TEMPLATES = [
|
|
|
30948
33179
|
},
|
|
30949
33180
|
{
|
|
30950
33181
|
name: "migration",
|
|
30951
|
-
version:
|
|
33182
|
+
version: BUILTIN_TEMPLATE_LIBRARY_VERSION,
|
|
30952
33183
|
category: "ops",
|
|
30953
33184
|
description: "Schema/data migration workflow",
|
|
30954
33185
|
variables: [
|
|
@@ -30965,7 +33196,7 @@ var BUILTIN_TEMPLATES = [
|
|
|
30965
33196
|
},
|
|
30966
33197
|
{
|
|
30967
33198
|
name: "incident-response",
|
|
30968
|
-
version:
|
|
33199
|
+
version: BUILTIN_TEMPLATE_LIBRARY_VERSION,
|
|
30969
33200
|
category: "ops",
|
|
30970
33201
|
description: "Incident triage, mitigation, postmortem",
|
|
30971
33202
|
variables: [{ name: "incident", required: true, description: "Incident summary" }],
|
|
@@ -31036,9 +33267,9 @@ function exportBuiltinTemplateFiles() {
|
|
|
31036
33267
|
function writeBuiltinTemplateFiles(directory) {
|
|
31037
33268
|
mkdirSync15(directory, { recursive: true });
|
|
31038
33269
|
const files = [];
|
|
31039
|
-
for (const
|
|
31040
|
-
const path = join15(directory,
|
|
31041
|
-
writeFileSync13(path, `${JSON.stringify(
|
|
33270
|
+
for (const entry2 of exportBuiltinTemplateFiles()) {
|
|
33271
|
+
const path = join15(directory, entry2.filename);
|
|
33272
|
+
writeFileSync13(path, `${JSON.stringify(entry2.template, null, 2)}
|
|
31042
33273
|
`, "utf-8");
|
|
31043
33274
|
files.push(path);
|
|
31044
33275
|
}
|
|
@@ -31160,8 +33391,14 @@ function getTemplateLibraryDocs() {
|
|
|
31160
33391
|
Marketplace-free bundled workflows \u2014 no network required.
|
|
31161
33392
|
|
|
31162
33393
|
## Categories
|
|
31163
|
-
- **
|
|
31164
|
-
- **
|
|
33394
|
+
- **bug-fix** \u2014 defect reproduction, diagnosis, test, implementation, release
|
|
33395
|
+
- **feature** \u2014 feature-implementation
|
|
33396
|
+
- **security** \u2014 security-review
|
|
33397
|
+
- **release** \u2014 release verification and publishing
|
|
33398
|
+
- **migration** \u2014 schema and data migration
|
|
33399
|
+
- **incident** \u2014 incident and incident-response
|
|
33400
|
+
- **docs** \u2014 docs-refresh
|
|
33401
|
+
- **qa** \u2014 QA matrix, checks, defects, signoff
|
|
31165
33402
|
- **project** \u2014 open-source-project bootstrap
|
|
31166
33403
|
|
|
31167
33404
|
## CLI
|
|
@@ -31264,8 +33501,8 @@ function buildMachineTopologyReport(db) {
|
|
|
31264
33501
|
schema_version: MACHINE_TOPOLOGY_SCHEMA,
|
|
31265
33502
|
id: m.id,
|
|
31266
33503
|
name: m.name,
|
|
31267
|
-
hostname: m.hostname,
|
|
31268
|
-
platform: m.platform,
|
|
33504
|
+
hostname: m.hostname ?? m.name,
|
|
33505
|
+
platform: m.platform ?? "unknown",
|
|
31269
33506
|
last_seen_at: m.last_seen_at,
|
|
31270
33507
|
is_local: m.id === localId,
|
|
31271
33508
|
active_agents: agents.filter((a) => !a.stale && a.machine_id === m.id).length,
|
|
@@ -31313,9 +33550,9 @@ todos machines topology # full diagnostic report
|
|
|
31313
33550
|
`;
|
|
31314
33551
|
}
|
|
31315
33552
|
// src/lib/environment-snapshots.ts
|
|
31316
|
-
import { createHash as
|
|
33553
|
+
import { createHash as createHash11 } from "crypto";
|
|
31317
33554
|
import { existsSync as existsSync19, readFileSync as readFileSync19, statSync as statSync7 } from "fs";
|
|
31318
|
-
import { hostname as hostname3, platform
|
|
33555
|
+
import { hostname as hostname3, platform, arch } from "os";
|
|
31319
33556
|
import { dirname as dirname15, join as join16, resolve as resolve14 } from "path";
|
|
31320
33557
|
import { tmpdir as tmpdir2 } from "os";
|
|
31321
33558
|
init_database();
|
|
@@ -31338,7 +33575,7 @@ var CONFIG_FILES = [
|
|
|
31338
33575
|
"dashboard/vite.config.ts"
|
|
31339
33576
|
];
|
|
31340
33577
|
function sha2565(value) {
|
|
31341
|
-
return
|
|
33578
|
+
return createHash11("sha256").update(value).digest("hex");
|
|
31342
33579
|
}
|
|
31343
33580
|
function fileRecord(root, relativePath) {
|
|
31344
33581
|
const path = join16(root, relativePath);
|
|
@@ -31473,7 +33710,7 @@ function captureEnvironmentSnapshot(input = {}) {
|
|
|
31473
33710
|
schema_version: 1,
|
|
31474
33711
|
captured_at: input.now ? new Date(input.now).toISOString() : new Date().toISOString(),
|
|
31475
33712
|
root,
|
|
31476
|
-
machine: { hostname: hostname3(), platform:
|
|
33713
|
+
machine: { hostname: hostname3(), platform: platform(), arch: arch() },
|
|
31477
33714
|
target: {
|
|
31478
33715
|
task_id: input.task_id ?? null,
|
|
31479
33716
|
run_id: input.run_id ?? null,
|
|
@@ -31562,7 +33799,7 @@ function diffFiles(left, right) {
|
|
|
31562
33799
|
const leftMap = keyed(left);
|
|
31563
33800
|
const rightMap = keyed(right);
|
|
31564
33801
|
const paths = [...new Set([...leftMap.keys(), ...rightMap.keys()])].sort((a, b) => a.localeCompare(b));
|
|
31565
|
-
return paths.map((path) => ({ path, left_sha256: leftMap.get(path)?.sha256 ?? null, right_sha256: rightMap.get(path)?.sha256 ?? null })).filter((
|
|
33802
|
+
return paths.map((path) => ({ path, left_sha256: leftMap.get(path)?.sha256 ?? null, right_sha256: rightMap.get(path)?.sha256 ?? null })).filter((entry2) => entry2.left_sha256 !== entry2.right_sha256);
|
|
31566
33803
|
}
|
|
31567
33804
|
function compareEnvironmentSnapshots(left, right) {
|
|
31568
33805
|
const warnings = [];
|
|
@@ -31588,7 +33825,7 @@ function compareEnvironmentSnapshotFiles(leftPath, rightPath) {
|
|
|
31588
33825
|
}
|
|
31589
33826
|
// src/lib/decision-records.ts
|
|
31590
33827
|
init_database();
|
|
31591
|
-
import { createHash as
|
|
33828
|
+
import { createHash as createHash12 } from "crypto";
|
|
31592
33829
|
import { mkdirSync as mkdirSync17, writeFileSync as writeFileSync15 } from "fs";
|
|
31593
33830
|
import { dirname as dirname16, join as join17 } from "path";
|
|
31594
33831
|
var DECISION_RECORD_SCHEMA = "todos.decision_record.v1";
|
|
@@ -31644,7 +33881,7 @@ function rowToDecisionRecord(row) {
|
|
|
31644
33881
|
}
|
|
31645
33882
|
function stableSnapshotHash(payload) {
|
|
31646
33883
|
const { captured_at: _capturedAt, ...rest } = payload;
|
|
31647
|
-
return
|
|
33884
|
+
return createHash12("sha256").update(JSON.stringify(rest)).digest("hex");
|
|
31648
33885
|
}
|
|
31649
33886
|
function createDecisionRecord(input, db) {
|
|
31650
33887
|
const d = db || getDatabase();
|
|
@@ -32024,7 +34261,7 @@ function buildReportExportData(input, db) {
|
|
|
32024
34261
|
let taskId = input.task_id ?? null;
|
|
32025
34262
|
switch (input.kind) {
|
|
32026
34263
|
case "project": {
|
|
32027
|
-
const project = projectId ? getProject(projectId, d) : listProjects(
|
|
34264
|
+
const project = projectId ? getProject(projectId, d) : listProjects(d)[0];
|
|
32028
34265
|
if (!project)
|
|
32029
34266
|
throw new Error("Project not found");
|
|
32030
34267
|
projectId = project.id;
|
|
@@ -32112,7 +34349,7 @@ function buildReportExportData(input, db) {
|
|
|
32112
34349
|
break;
|
|
32113
34350
|
}
|
|
32114
34351
|
case "roadmap": {
|
|
32115
|
-
const project = projectId ? getProject(projectId, d) : listProjects(
|
|
34352
|
+
const project = projectId ? getProject(projectId, d) : listProjects(d)[0];
|
|
32116
34353
|
if (!project)
|
|
32117
34354
|
throw new Error("Project not found");
|
|
32118
34355
|
projectId = project.id;
|
|
@@ -32747,7 +34984,7 @@ function applyFailureTriage(input, db) {
|
|
|
32747
34984
|
}
|
|
32748
34985
|
if (action === "escalate") {
|
|
32749
34986
|
const meta = { ...task2.metadata, _triage_escalated: { at: now(), reason, classification } };
|
|
32750
|
-
const escalated = updateTask(task2.id, { priority: "
|
|
34987
|
+
const escalated = updateTask(task2.id, { priority: "critical", metadata: meta, version: task2.version }, d);
|
|
32751
34988
|
return { schema_version: FAILURE_TRIAGE_SCHEMA, action, classification, playbook, task: escalated, escalated: true };
|
|
32752
34989
|
}
|
|
32753
34990
|
}
|
|
@@ -32971,7 +35208,7 @@ function storePath(cwd) {
|
|
|
32971
35208
|
function versionsDir(cwd) {
|
|
32972
35209
|
return join19(storeDir(cwd), "versions");
|
|
32973
35210
|
}
|
|
32974
|
-
function
|
|
35211
|
+
function slugify5(name) {
|
|
32975
35212
|
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
32976
35213
|
}
|
|
32977
35214
|
function emptyStore2() {
|
|
@@ -33010,7 +35247,7 @@ function createUserScaffold(input, db, cwd) {
|
|
|
33010
35247
|
const store = loadUserScaffoldStore(cwd);
|
|
33011
35248
|
const id = uuid();
|
|
33012
35249
|
const ts = now();
|
|
33013
|
-
const slug =
|
|
35250
|
+
const slug = slugify5(input.name);
|
|
33014
35251
|
if (Object.values(store.scaffolds).some((s) => s.slug === slug)) {
|
|
33015
35252
|
throw new Error(`Scaffold slug already exists: ${slug}`);
|
|
33016
35253
|
}
|
|
@@ -33047,8 +35284,8 @@ function updateUserScaffold(idOrSlug, updates, db, cwd) {
|
|
|
33047
35284
|
if (!existing)
|
|
33048
35285
|
throw new Error(`Scaffold not found: ${idOrSlug}`);
|
|
33049
35286
|
snapshotVersion(existing, cwd);
|
|
33050
|
-
if (updates.name &&
|
|
33051
|
-
const newSlug =
|
|
35287
|
+
if (updates.name && slugify5(updates.name) !== existing.slug) {
|
|
35288
|
+
const newSlug = slugify5(updates.name);
|
|
33052
35289
|
if (Object.values(store.scaffolds).some((s) => s.id !== existing.id && s.slug === newSlug)) {
|
|
33053
35290
|
throw new Error(`Scaffold slug conflict: ${newSlug}`);
|
|
33054
35291
|
}
|
|
@@ -33244,7 +35481,7 @@ import { mkdtempSync } from "fs";
|
|
|
33244
35481
|
import { join as join20 } from "path";
|
|
33245
35482
|
import { tmpdir as tmpdir3 } from "os";
|
|
33246
35483
|
var AGENT_WORKFLOW_DEMO_SCHEMA = "todos.agent_workflow_demo.v1";
|
|
33247
|
-
var DEMO_DEFAULT_AGENT = "
|
|
35484
|
+
var DEMO_DEFAULT_AGENT = "demoagent";
|
|
33248
35485
|
var DEMO_DEFAULT_PROJECT = "Agent Workflow Demo";
|
|
33249
35486
|
var DEMO_PROJECT_PATH = "/tmp/todos-agent-workflow-demo";
|
|
33250
35487
|
function pushStep(steps, action, detail, status = "ok") {
|
|
@@ -33350,7 +35587,7 @@ function runAgentWorkflowDemo(options = {}) {
|
|
|
33350
35587
|
agent_id: agent.id,
|
|
33351
35588
|
evidence: { demo: true, source: "agent_workflow_demo" }
|
|
33352
35589
|
}, d);
|
|
33353
|
-
pushStep(steps, "enqueue_agent_run", `Queued agent run on task ${third.short_id ?? third.id.slice(0, 8)}`);
|
|
35590
|
+
pushStep(steps, "enqueue_agent_run", `Queued agent run ${queuedRun.id.slice(0, 8)} on task ${third.short_id ?? third.id.slice(0, 8)}`);
|
|
33354
35591
|
const claimedRun = claimNextAgentRun(agent.id, { adapter: demoAdapter }, d);
|
|
33355
35592
|
if (!claimedRun)
|
|
33356
35593
|
throw new Error("Expected queued agent run");
|
|
@@ -33399,7 +35636,7 @@ function runAgentWorkflowDemo(options = {}) {
|
|
|
33399
35636
|
id: completedRun.id,
|
|
33400
35637
|
status: completedRun.status,
|
|
33401
35638
|
task_id: completedRun.task_id,
|
|
33402
|
-
adapter: completedRun.adapter
|
|
35639
|
+
adapter: completedRun.adapter ?? demoAdapter
|
|
33403
35640
|
},
|
|
33404
35641
|
run_record: {
|
|
33405
35642
|
id: finishedRecord.id,
|
|
@@ -33877,13 +36114,13 @@ var ALL_MCP_TOOLS = [
|
|
|
33877
36114
|
"sync_todos_md",
|
|
33878
36115
|
"task_context",
|
|
33879
36116
|
"template_history",
|
|
33880
|
-
"todos_cloud_conflicts",
|
|
33881
|
-
"todos_cloud_feedback",
|
|
33882
|
-
"todos_cloud_pull",
|
|
33883
|
-
"todos_cloud_push",
|
|
33884
|
-
"todos_cloud_status",
|
|
33885
36117
|
"todos_inbox",
|
|
33886
36118
|
"todos_retro",
|
|
36119
|
+
"todos_storage_conflicts",
|
|
36120
|
+
"todos_storage_feedback",
|
|
36121
|
+
"todos_storage_pull",
|
|
36122
|
+
"todos_storage_push",
|
|
36123
|
+
"todos_storage_status",
|
|
33887
36124
|
"trust_workspace",
|
|
33888
36125
|
"unarchive_agent",
|
|
33889
36126
|
"unarchive_task",
|
|
@@ -34021,10 +36258,10 @@ var MCP_GROUP_DEFS = [
|
|
|
34021
36258
|
match: (t) => /sandbox|trust|secret|crypto|verification|policy_pack|redact|scan_/.test(t)
|
|
34022
36259
|
},
|
|
34023
36260
|
{
|
|
34024
|
-
id: "
|
|
34025
|
-
name: "
|
|
34026
|
-
description: "Optional
|
|
34027
|
-
match: (t) => t.startsWith("
|
|
36261
|
+
id: "storage",
|
|
36262
|
+
name: "Storage bridge",
|
|
36263
|
+
description: "Optional storage sync tools (admin profile).",
|
|
36264
|
+
match: (t) => t.startsWith("todos_storage_") || t === "sync_all" || t === "migrate_pg"
|
|
34028
36265
|
},
|
|
34029
36266
|
{
|
|
34030
36267
|
id: "workflow",
|
|
@@ -34249,7 +36486,7 @@ function normalizeFeatureManifest(manifest, generatedAt = "2026-01-01T00:00:00.0
|
|
|
34249
36486
|
mcp: {
|
|
34250
36487
|
total_tools: manifest.mcp.total_tools,
|
|
34251
36488
|
tools_for_profile_count: manifest.mcp.tools_for_profile_count,
|
|
34252
|
-
tool_group_counts: Object.fromEntries(manifest.mcp.tool_groups.map((g) => [g.id, g.tools.length]).sort(([a], [b]) => a.localeCompare(b)))
|
|
36489
|
+
tool_group_counts: Object.fromEntries(manifest.mcp.tool_groups.map((g) => [g.id, g.tools.length]).sort(([a], [b]) => String(a).localeCompare(String(b))))
|
|
34253
36490
|
},
|
|
34254
36491
|
profiles: manifest.profiles.map((p) => p.name),
|
|
34255
36492
|
env_vars: manifest.env_vars.map((e) => e.name),
|
|
@@ -34321,7 +36558,7 @@ Set \`TODOS_PROFILE\` to control which MCP tools load:
|
|
|
34321
36558
|
| agent_safe | Claim/complete workflow without destructive ops |
|
|
34322
36559
|
| standard | Default minus admin/template/webhook tools |
|
|
34323
36560
|
| full | All local tools |
|
|
34324
|
-
| admin | Includes migrations and
|
|
36561
|
+
| admin | Includes migrations and storage bridge |
|
|
34325
36562
|
|
|
34326
36563
|
Schema: \`${FEATURE_MANIFEST_SCHEMA}\`
|
|
34327
36564
|
`;
|
|
@@ -34843,8 +37080,8 @@ function getLeaderboard(opts, db) {
|
|
|
34843
37080
|
}
|
|
34844
37081
|
entries.sort((a, b) => b.composite_score - a.composite_score);
|
|
34845
37082
|
const limit = opts?.limit || 20;
|
|
34846
|
-
return entries.slice(0, limit).map((
|
|
34847
|
-
...
|
|
37083
|
+
return entries.slice(0, limit).map((entry2, idx) => ({
|
|
37084
|
+
...entry2,
|
|
34848
37085
|
rank: idx + 1
|
|
34849
37086
|
}));
|
|
34850
37087
|
}
|
|
@@ -36115,7 +38352,7 @@ function syncWithAgents(agents, taskListIdByAgent, projectId, direction = "both"
|
|
|
36115
38352
|
}
|
|
36116
38353
|
// src/lib/extract.ts
|
|
36117
38354
|
import { existsSync as existsSync24, readFileSync as readFileSync23, statSync as statSync8 } from "fs";
|
|
36118
|
-
import { createHash as
|
|
38355
|
+
import { createHash as createHash13 } from "crypto";
|
|
36119
38356
|
import { relative as relative6, resolve as resolve15, join as join23 } from "path";
|
|
36120
38357
|
var EXTRACT_TAGS = ["TODO", "FIXME", "HACK", "XXX", "BUG", "NOTE"];
|
|
36121
38358
|
var DEFAULT_EXTENSIONS = new Set([
|
|
@@ -36180,7 +38417,7 @@ var SKIP_DIRS2 = new Set([
|
|
|
36180
38417
|
".parcel-cache"
|
|
36181
38418
|
]);
|
|
36182
38419
|
function stableHash(value) {
|
|
36183
|
-
return
|
|
38420
|
+
return createHash13("sha256").update(value).digest("hex");
|
|
36184
38421
|
}
|
|
36185
38422
|
function normalizePathForMatch(value) {
|
|
36186
38423
|
return value.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
@@ -36305,19 +38542,19 @@ function collectFiles(basePath, extensions, excludes, respectGitignore) {
|
|
|
36305
38542
|
const files = [];
|
|
36306
38543
|
const ignorePatterns = respectGitignore ? readGitignorePatterns(basePath) : [];
|
|
36307
38544
|
const allExcludes = [...ignorePatterns, ...excludes];
|
|
36308
|
-
for (const
|
|
36309
|
-
const parts =
|
|
38545
|
+
for (const entry2 of glob.scanSync({ cwd: basePath, onlyFiles: true, dot: false })) {
|
|
38546
|
+
const parts = entry2.split("/");
|
|
36310
38547
|
if (parts.some((p) => SKIP_DIRS2.has(p)))
|
|
36311
38548
|
continue;
|
|
36312
|
-
if (matchesAnyPattern(
|
|
38549
|
+
if (matchesAnyPattern(entry2, allExcludes))
|
|
36313
38550
|
continue;
|
|
36314
|
-
const dotIdx =
|
|
38551
|
+
const dotIdx = entry2.lastIndexOf(".");
|
|
36315
38552
|
if (dotIdx === -1)
|
|
36316
38553
|
continue;
|
|
36317
|
-
const ext =
|
|
38554
|
+
const ext = entry2.slice(dotIdx);
|
|
36318
38555
|
if (!extensions.has(ext))
|
|
36319
38556
|
continue;
|
|
36320
|
-
files.push(
|
|
38557
|
+
files.push(entry2);
|
|
36321
38558
|
}
|
|
36322
38559
|
return files.sort();
|
|
36323
38560
|
}
|
|
@@ -37068,7 +39305,7 @@ function renderWorkflowStatesMarkdown(states = listWorkflowStates()) {
|
|
|
37068
39305
|
}
|
|
37069
39306
|
// src/lib/agent-replay-simulator.ts
|
|
37070
39307
|
init_redaction();
|
|
37071
|
-
import { createHash as
|
|
39308
|
+
import { createHash as createHash14 } from "crypto";
|
|
37072
39309
|
import { readFileSync as readFileSync24 } from "fs";
|
|
37073
39310
|
function isObject(value) {
|
|
37074
39311
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
@@ -37090,7 +39327,7 @@ function stable2(value) {
|
|
|
37090
39327
|
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, stable2(value[key])]));
|
|
37091
39328
|
}
|
|
37092
39329
|
function fingerprint3(value) {
|
|
37093
|
-
return
|
|
39330
|
+
return createHash14("sha256").update(JSON.stringify(stable2(value))).digest("hex");
|
|
37094
39331
|
}
|
|
37095
39332
|
function unpackFixture(input) {
|
|
37096
39333
|
if (!isObject(input))
|
|
@@ -37329,7 +39566,7 @@ function renderAgentReplaySimulationMarkdown(simulation) {
|
|
|
37329
39566
|
}
|
|
37330
39567
|
// src/lib/local-extensions.ts
|
|
37331
39568
|
init_config();
|
|
37332
|
-
import { createHash as
|
|
39569
|
+
import { createHash as createHash15, createVerify } from "crypto";
|
|
37333
39570
|
import { existsSync as existsSync25, readdirSync as readdirSync5, readFileSync as readFileSync25, statSync as statSync9 } from "fs";
|
|
37334
39571
|
import { basename as basename5, join as join24, resolve as resolve16 } from "path";
|
|
37335
39572
|
init_redaction();
|
|
@@ -37416,7 +39653,7 @@ function parseJson(path) {
|
|
|
37416
39653
|
return JSON.parse(readFileSync25(path, "utf8"));
|
|
37417
39654
|
}
|
|
37418
39655
|
function sha2566(bytes) {
|
|
37419
|
-
return `sha256:${
|
|
39656
|
+
return `sha256:${createHash15("sha256").update(bytes).digest("hex")}`;
|
|
37420
39657
|
}
|
|
37421
39658
|
function compareVersions(a, b) {
|
|
37422
39659
|
const left = a.split(".").map((part) => Number.parseInt(part, 10) || 0);
|
|
@@ -37716,11 +39953,11 @@ function projectExtensionSources(projectPath) {
|
|
|
37716
39953
|
];
|
|
37717
39954
|
const extensionDir = join24(root, ".todos", "extensions");
|
|
37718
39955
|
if (existsSync25(extensionDir)) {
|
|
37719
|
-
for (const
|
|
37720
|
-
if (
|
|
39956
|
+
for (const entry2 of readdirSync5(extensionDir)) {
|
|
39957
|
+
if (entry2.startsWith("."))
|
|
37721
39958
|
continue;
|
|
37722
|
-
const full = join24(extensionDir,
|
|
37723
|
-
if (statSync9(full).isDirectory() ||
|
|
39959
|
+
const full = join24(extensionDir, entry2);
|
|
39960
|
+
if (statSync9(full).isDirectory() || entry2.endsWith(".json"))
|
|
37724
39961
|
candidates.push(full);
|
|
37725
39962
|
}
|
|
37726
39963
|
}
|
|
@@ -38297,16 +40534,16 @@ function getExpiredArtifactFileCandidates(input, generatedAt, db, deletedRunIds)
|
|
|
38297
40534
|
}
|
|
38298
40535
|
const candidates = [];
|
|
38299
40536
|
for (const [relativePath, entries] of byPath) {
|
|
38300
|
-
const retainedEntries = entries.filter((
|
|
40537
|
+
const retainedEntries = entries.filter((entry2) => !deletedRunIds.has(entry2.row.run_id) && (!entry2.expires_at || entry2.expires_at > generatedAt));
|
|
38301
40538
|
if (retainedEntries.length > 0)
|
|
38302
40539
|
continue;
|
|
38303
|
-
const removableEntries = entries.filter((
|
|
40540
|
+
const removableEntries = entries.filter((entry2) => deletedRunIds.has(entry2.row.run_id) || Boolean(entry2.expires_at && entry2.expires_at <= generatedAt));
|
|
38304
40541
|
if (removableEntries.length === 0)
|
|
38305
40542
|
continue;
|
|
38306
|
-
const latestExpiry = removableEntries.map((
|
|
40543
|
+
const latestExpiry = removableEntries.map((entry2) => entry2.expires_at).filter((expiresAt) => typeof expiresAt === "string").sort().at(-1) ?? generatedAt;
|
|
38307
40544
|
candidates.push({
|
|
38308
40545
|
relative_path: relativePath,
|
|
38309
|
-
artifact_ids: removableEntries.map((
|
|
40546
|
+
artifact_ids: removableEntries.map((entry2) => entry2.row.id).sort(),
|
|
38310
40547
|
expires_at: latestExpiry,
|
|
38311
40548
|
size_bytes: typeof removableEntries[0].store.size_bytes === "number" ? removableEntries[0].store.size_bytes : null
|
|
38312
40549
|
});
|
|
@@ -39114,7 +41351,7 @@ function removeCapacityProfile(idOrAgent, projectId) {
|
|
|
39114
41351
|
function getPlanningForecast(input = {}, db) {
|
|
39115
41352
|
const startDate = input.start_date ? dateOnly(input.start_date) : todayIso();
|
|
39116
41353
|
const tasks = listTasks({ project_id: input.project_id, plan_id: input.plan_id, limit: 1e4 }, db).filter((task2) => taskMatchesAgent(task2, input.agent_id));
|
|
39117
|
-
const reportByTask = new Map(getTimeReport({ project_id: input.project_id, plan_id: input.plan_id, agent_id: input.agent_id, include_open: true }, db).map((
|
|
41354
|
+
const reportByTask = new Map(getTimeReport({ project_id: input.project_id, plan_id: input.plan_id, agent_id: input.agent_id, include_open: true }, db).map((entry2) => [entry2.task_id, entry2]));
|
|
39118
41355
|
const profiles = listCapacityProfiles({ agent_id: input.agent_id, project_id: input.project_id });
|
|
39119
41356
|
const globalProfiles = input.project_id ? listCapacityProfiles({ agent_id: input.agent_id }).filter((profile) => profile.project_id === null) : [];
|
|
39120
41357
|
const capacityProfiles = profiles.length > 0 ? profiles : globalProfiles;
|
|
@@ -39142,7 +41379,7 @@ function getPlanningForecast(input = {}, db) {
|
|
|
39142
41379
|
const estimatedMinutes = forecastTasks.reduce((sum, task2) => sum + (task2.estimated_minutes ?? 0), 0);
|
|
39143
41380
|
const remainingEstimatedMinutes = forecastTasks.reduce((sum, task2) => sum + task2.remaining_estimated_minutes, 0);
|
|
39144
41381
|
const actualMinutes = forecastTasks.reduce((sum, task2) => sum + (task2.actual_minutes ?? 0), 0);
|
|
39145
|
-
const loggedMinutes = [...reportByTask.values()].reduce((sum,
|
|
41382
|
+
const loggedMinutes = [...reportByTask.values()].reduce((sum, entry2) => sum + entry2.logged_minutes, 0);
|
|
39146
41383
|
const missingEstimateCount = forecastTasks.filter((task2) => task2.missing_estimate).length;
|
|
39147
41384
|
const overdueOpenTaskCount = forecastTasks.filter((task2) => task2.overdue).length;
|
|
39148
41385
|
const forecastWorkDays = capacityMinutesPerDay > 0 ? Math.ceil(remainingEstimatedMinutes / capacityMinutesPerDay) : null;
|
|
@@ -39351,7 +41588,7 @@ function fishEscape(value) {
|
|
|
39351
41588
|
return value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"");
|
|
39352
41589
|
}
|
|
39353
41590
|
function rootCommandNames(entries) {
|
|
39354
|
-
return entries.filter((
|
|
41591
|
+
return entries.filter((entry2) => entry2.path.length === 1).map((entry2) => entry2.path[0]).filter((value) => Boolean(value));
|
|
39355
41592
|
}
|
|
39356
41593
|
function optionFlags(options) {
|
|
39357
41594
|
const flags = new Set;
|
|
@@ -39363,17 +41600,17 @@ function optionFlags(options) {
|
|
|
39363
41600
|
}
|
|
39364
41601
|
return [...flags].sort();
|
|
39365
41602
|
}
|
|
39366
|
-
function commandOptions(program,
|
|
39367
|
-
return optionFlags([...program.options.map(optionEntry), ...
|
|
41603
|
+
function commandOptions(program, entry2) {
|
|
41604
|
+
return optionFlags([...program.options.map(optionEntry), ...entry2.options]);
|
|
39368
41605
|
}
|
|
39369
41606
|
function generateCompletionScript(program, shell) {
|
|
39370
41607
|
const entries = collectCliCommandEntries(program);
|
|
39371
41608
|
const roots = rootCommandNames(entries);
|
|
39372
41609
|
if (shell === "bash") {
|
|
39373
|
-
const rootCases = entries.filter((
|
|
39374
|
-
const children = entries.filter((child) => child.path.length === 2 && child.path[0] ===
|
|
39375
|
-
const options = commandOptions(program,
|
|
39376
|
-
return ` ${
|
|
41610
|
+
const rootCases = entries.filter((entry2) => entry2.path.length === 1).map((entry2) => {
|
|
41611
|
+
const children = entries.filter((child) => child.path.length === 2 && child.path[0] === entry2.path[0]).map((child) => child.path[1]).filter((value) => Boolean(value));
|
|
41612
|
+
const options = commandOptions(program, entry2);
|
|
41613
|
+
return ` ${entry2.path[0]})
|
|
39377
41614
|
COMPREPLY=($(compgen -W '${shellWords([...children, ...options])}' -- "$cur"))
|
|
39378
41615
|
return
|
|
39379
41616
|
;;`;
|
|
@@ -39400,14 +41637,14 @@ function generateCompletionScript(program, shell) {
|
|
|
39400
41637
|
`);
|
|
39401
41638
|
}
|
|
39402
41639
|
if (shell === "zsh") {
|
|
39403
|
-
const commandEntries = entries.filter((
|
|
41640
|
+
const commandEntries = entries.filter((entry2) => entry2.path.length === 1).map((entry2) => zshEntry(entry2.path[0] || "", entry2.description));
|
|
39404
41641
|
const optionEntries = optionFlags(program.options.map(optionEntry)).map((flag) => `'${flag}'`);
|
|
39405
|
-
const childCases = entries.filter((
|
|
39406
|
-
const children = entries.filter((child) => child.path.length === 2 && child.path[0] ===
|
|
41642
|
+
const childCases = entries.filter((entry2) => entry2.path.length === 1).map((entry2) => {
|
|
41643
|
+
const children = entries.filter((child) => child.path.length === 2 && child.path[0] === entry2.path[0]);
|
|
39407
41644
|
if (children.length === 0)
|
|
39408
41645
|
return "";
|
|
39409
41646
|
const childEntries = children.map((child) => zshEntry(child.path[1] || "", child.description)).join(" ");
|
|
39410
|
-
return ` ${
|
|
41647
|
+
return ` ${entry2.path[0]}) local -a subcommands; subcommands=(${childEntries}); _describe 'subcommand' subcommands ;;`;
|
|
39411
41648
|
}).filter(Boolean).join(`
|
|
39412
41649
|
`);
|
|
39413
41650
|
return [
|
|
@@ -39443,11 +41680,11 @@ function generateCompletionScript(program, shell) {
|
|
|
39443
41680
|
parts.push(`-d "${fishEscape(option.description)}"`);
|
|
39444
41681
|
fishLines.push(parts.join(" "));
|
|
39445
41682
|
}
|
|
39446
|
-
for (const
|
|
39447
|
-
if (
|
|
39448
|
-
fishLines.push(`complete -c todos -n "__fish_use_subcommand" -a "${
|
|
39449
|
-
} else if (
|
|
39450
|
-
fishLines.push(`complete -c todos -n "__fish_seen_subcommand_from ${
|
|
41683
|
+
for (const entry2 of entries) {
|
|
41684
|
+
if (entry2.path.length === 1) {
|
|
41685
|
+
fishLines.push(`complete -c todos -n "__fish_use_subcommand" -a "${entry2.path[0]}" -d "${fishEscape(entry2.description)}"`);
|
|
41686
|
+
} else if (entry2.path.length === 2) {
|
|
41687
|
+
fishLines.push(`complete -c todos -n "__fish_seen_subcommand_from ${entry2.path[0]}" -a "${entry2.path[1]}" -d "${fishEscape(entry2.description)}"`);
|
|
39451
41688
|
}
|
|
39452
41689
|
}
|
|
39453
41690
|
fishLines.push("");
|
|
@@ -39499,6 +41736,7 @@ export {
|
|
|
39499
41736
|
upsertEncryptionProfile,
|
|
39500
41737
|
upsertCapacityProfile,
|
|
39501
41738
|
upsertAgentRunAdapter,
|
|
41739
|
+
uploadRunArtifactsToS3,
|
|
39502
41740
|
updateUserScaffold,
|
|
39503
41741
|
updateTemplate,
|
|
39504
41742
|
updateTaskList,
|
|
@@ -39554,6 +41792,7 @@ export {
|
|
|
39554
41792
|
slugify,
|
|
39555
41793
|
simulateAgentReplayFile,
|
|
39556
41794
|
simulateAgentReplay,
|
|
41795
|
+
signAwsV4Request,
|
|
39557
41796
|
shouldRegisterToolForProfile,
|
|
39558
41797
|
shouldRegisterToolForProfile2 as shouldRegisterToolForAccessProfile,
|
|
39559
41798
|
setupEphemeralDemoDb,
|
|
@@ -39691,6 +41930,7 @@ export {
|
|
|
39691
41930
|
redactHandoffPayload,
|
|
39692
41931
|
redactExportRecord,
|
|
39693
41932
|
redactEvidenceText,
|
|
41933
|
+
redactDatabaseUrl,
|
|
39694
41934
|
redactCommentContent,
|
|
39695
41935
|
redactActivityRecord,
|
|
39696
41936
|
recoverStaleLeases,
|
|
@@ -39717,11 +41957,15 @@ export {
|
|
|
39717
41957
|
previewInstalledTemplate,
|
|
39718
41958
|
previewInboxIntake,
|
|
39719
41959
|
previewBuiltinTemplate,
|
|
41960
|
+
postgresTodosSyncSchemaSql,
|
|
39720
41961
|
pollLocalSnapshots,
|
|
41962
|
+
planRunArtifactsS3Sync,
|
|
39721
41963
|
pauseFocusSession,
|
|
39722
41964
|
patrolTasks,
|
|
39723
41965
|
parseTmuxTarget,
|
|
41966
|
+
parseStorageMode,
|
|
39724
41967
|
parseRecurrenceRule,
|
|
41968
|
+
parseQuietHours,
|
|
39725
41969
|
parseNaturalLanguageTask,
|
|
39726
41970
|
parseIssueExport,
|
|
39727
41971
|
parseGoalCommand,
|
|
@@ -39747,6 +41991,7 @@ export {
|
|
|
39747
41991
|
mergeDuplicateTask,
|
|
39748
41992
|
materializePlanSteps,
|
|
39749
41993
|
matchCapabilities,
|
|
41994
|
+
looksSensitiveKey,
|
|
39750
41995
|
logTrace,
|
|
39751
41996
|
logTime,
|
|
39752
41997
|
logTaskChange,
|
|
@@ -39756,6 +42001,8 @@ export {
|
|
|
39756
42001
|
logActivity,
|
|
39757
42002
|
lockTask,
|
|
39758
42003
|
loadUserScaffoldStore,
|
|
42004
|
+
loadTodosStorageConfig,
|
|
42005
|
+
loadStorageConfig,
|
|
39759
42006
|
loadSandboxProfiles,
|
|
39760
42007
|
loadIssueExportFromFile,
|
|
39761
42008
|
loadConfig,
|
|
@@ -39856,6 +42103,7 @@ export {
|
|
|
39856
42103
|
linkRunArtifact,
|
|
39857
42104
|
issueToTask,
|
|
39858
42105
|
isValidRecurrenceRule,
|
|
42106
|
+
isTodosRemoteStorageEnabled,
|
|
39859
42107
|
isSnapshotStale,
|
|
39860
42108
|
isEncryptedValue,
|
|
39861
42109
|
isEncryptedBridgeBundle,
|
|
@@ -39873,6 +42121,7 @@ export {
|
|
|
39873
42121
|
importTemplate,
|
|
39874
42122
|
importTaskBoardBundle,
|
|
39875
42123
|
importStoredArtifactContent,
|
|
42124
|
+
importSqliteTodosStorageSnapshot,
|
|
39876
42125
|
importRoadmapBundle,
|
|
39877
42126
|
importOnboardingFixture,
|
|
39878
42127
|
importLocalBridgeBundle,
|
|
@@ -39897,6 +42146,10 @@ export {
|
|
|
39897
42146
|
getUnlockImpact,
|
|
39898
42147
|
getTraceStats,
|
|
39899
42148
|
getTopologyDocs,
|
|
42149
|
+
getTodosStorageMode,
|
|
42150
|
+
getTodosStorageEnvName,
|
|
42151
|
+
getTodosStorageDatabaseUrl,
|
|
42152
|
+
getTodosStorageDatabaseEnv,
|
|
39900
42153
|
getTimeReport,
|
|
39901
42154
|
getTimeLogs,
|
|
39902
42155
|
getTerminalNotificationRule,
|
|
@@ -39933,6 +42186,9 @@ export {
|
|
|
39933
42186
|
getTaskBoard,
|
|
39934
42187
|
getTask,
|
|
39935
42188
|
getStoredHandoffAsPacket,
|
|
42189
|
+
getStorageMode,
|
|
42190
|
+
getStorageDatabaseUrl,
|
|
42191
|
+
getStorageDatabaseEnv,
|
|
39936
42192
|
getStatus,
|
|
39937
42193
|
getStaleTasks,
|
|
39938
42194
|
getStaleTaskReport,
|
|
@@ -39979,6 +42235,8 @@ export {
|
|
|
39979
42235
|
getOnboardingFixture,
|
|
39980
42236
|
getNextTask,
|
|
39981
42237
|
getNextCycle,
|
|
42238
|
+
getNativeStorageSyncPlan,
|
|
42239
|
+
getNativeStorageStatus,
|
|
39982
42240
|
getMcpToolNames,
|
|
39983
42241
|
getMachineTopologyDiagnostics,
|
|
39984
42242
|
getMachineLocalPath,
|
|
@@ -40128,6 +42386,7 @@ export {
|
|
|
40128
42386
|
exportTaskFields,
|
|
40129
42387
|
exportTaskBoardBundle,
|
|
40130
42388
|
exportStoredArtifactContent,
|
|
42389
|
+
exportSqliteTodosStorageSnapshot,
|
|
40131
42390
|
exportSchemasToDirectory,
|
|
40132
42391
|
exportRunReplay,
|
|
40133
42392
|
exportRoadmapBundle,
|
|
@@ -40154,6 +42413,7 @@ export {
|
|
|
40154
42413
|
evaluateCondition,
|
|
40155
42414
|
ensureTaskList,
|
|
40156
42415
|
ensureProject,
|
|
42416
|
+
ensureEncryptionProfile,
|
|
40157
42417
|
enqueueAgentRun,
|
|
40158
42418
|
encryptionProfileStatus,
|
|
40159
42419
|
encryptValue,
|
|
@@ -40161,6 +42421,7 @@ export {
|
|
|
40161
42421
|
encryptSensitiveFields,
|
|
40162
42422
|
emitLocalEventHooksQuiet,
|
|
40163
42423
|
emitLocalEventHooks,
|
|
42424
|
+
downloadRunArtifactsFromS3,
|
|
40164
42425
|
dispatchWebhook,
|
|
40165
42426
|
dispatchToMultiple,
|
|
40166
42427
|
dismissReminder,
|
|
@@ -40203,6 +42464,8 @@ export {
|
|
|
40203
42464
|
createVerificationEvidence,
|
|
40204
42465
|
createUserScaffold,
|
|
40205
42466
|
createTuiDashboardSnapshot,
|
|
42467
|
+
createTodosStorageAdapter,
|
|
42468
|
+
createTodosS3ArtifactStore,
|
|
40206
42469
|
createTodosRegistry,
|
|
40207
42470
|
createTemplate,
|
|
40208
42471
|
createTaskList,
|
|
@@ -40222,6 +42485,8 @@ export {
|
|
|
40222
42485
|
createReminder,
|
|
40223
42486
|
createReleaseCompatibilityReport,
|
|
40224
42487
|
createProject,
|
|
42488
|
+
createPostgresTodosSyncStore,
|
|
42489
|
+
createPostgresTodosStorageAdapter,
|
|
40225
42490
|
createPlanWithSteps,
|
|
40226
42491
|
createPlan,
|
|
40227
42492
|
createOrg,
|
|
@@ -40240,6 +42505,7 @@ export {
|
|
|
40240
42505
|
createJsonContractsManifest,
|
|
40241
42506
|
createInboxItem,
|
|
40242
42507
|
createInboxIntake,
|
|
42508
|
+
createHybridTodosStorageAdapter,
|
|
40243
42509
|
createHandoffPacket,
|
|
40244
42510
|
createHandoff,
|
|
40245
42511
|
createGoalWorkflow,
|
|
@@ -40306,6 +42572,8 @@ export {
|
|
|
40306
42572
|
bulkCreateTasks,
|
|
40307
42573
|
bulkAddTaskFiles,
|
|
40308
42574
|
buildTaskBoardSnapshot,
|
|
42575
|
+
buildS3ObjectUrl,
|
|
42576
|
+
buildS3ObjectKey,
|
|
40309
42577
|
buildRunReplayBundle,
|
|
40310
42578
|
buildResourceSnapshot,
|
|
40311
42579
|
buildReportExportData,
|
|
@@ -40325,6 +42593,7 @@ export {
|
|
|
40325
42593
|
attachPlanToProject,
|
|
40326
42594
|
assignLabelToTask,
|
|
40327
42595
|
assertToolAllowed,
|
|
42596
|
+
assertTodosRemoteStorageConfig,
|
|
40328
42597
|
assertNoSecrets,
|
|
40329
42598
|
assertHeadlessOutboundUrl,
|
|
40330
42599
|
assertExportProfileAllowed,
|
|
@@ -40366,6 +42635,9 @@ export {
|
|
|
40366
42635
|
TaskNotFoundError,
|
|
40367
42636
|
TaskListNotFoundError,
|
|
40368
42637
|
TUI_DASHBOARD_VIEWS,
|
|
42638
|
+
TODOS_STORAGE_TABLES,
|
|
42639
|
+
TODOS_STORAGE_FALLBACK_ENV,
|
|
42640
|
+
TODOS_STORAGE_ENV,
|
|
40369
42641
|
TODOS_SDK_INTEGRATION_FIXTURE_SCHEMA_VERSION,
|
|
40370
42642
|
TODOS_SDK_INTEGRATION_FIXTURE_GENERATED_AT,
|
|
40371
42643
|
TODOS_REGISTRY,
|
|
@@ -40396,6 +42668,7 @@ export {
|
|
|
40396
42668
|
TASK_STATUSES,
|
|
40397
42669
|
TASK_SCHEDULING_SCHEMA,
|
|
40398
42670
|
TASK_PRIORITIES,
|
|
42671
|
+
STORAGE_TABLES,
|
|
40399
42672
|
SECRET_REDACTION_SCHEMA,
|
|
40400
42673
|
SCHEMA_SEMVER,
|
|
40401
42674
|
SCHEMA_ENTITIES,
|