@hasna/todos 0.15.35 → 0.15.41
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/dist/cli/cloud-router.d.ts +52 -18
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/agent-commands.d.ts.map +1 -1
- package/dist/cli/commands/plan-template-commands.d.ts.map +1 -1
- package/dist/cli/commands/project-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-subtree-transfer-commands.d.ts +3 -0
- package/dist/cli/commands/task-subtree-transfer-commands.d.ts.map +1 -0
- package/dist/cli/index.js +3299 -796
- package/dist/cli/stage-a.d.ts.map +1 -1
- package/dist/contracts.js +62 -54
- package/dist/db/plan-comments.d.ts +14 -0
- package/dist/db/plan-comments.d.ts.map +1 -0
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/task-crud.d.ts.map +1 -1
- package/dist/db/task-lifecycle.d.ts.map +1 -1
- package/dist/db/task-lists.d.ts.map +1 -1
- package/dist/db/task-relations.d.ts.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1787 -309
- package/dist/lib/comment-cursor.d.ts +19 -0
- package/dist/lib/comment-cursor.d.ts.map +1 -1
- package/dist/lib/import-export-bridge.d.ts.map +1 -1
- package/dist/mcp/index.js +2273 -471
- package/dist/mcp.js +8 -4
- package/dist/project-registration.js +207 -63
- package/dist/registry.d.ts +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +69 -54
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/index.d.ts +1 -1
- package/dist/sdk/index.d.ts.map +1 -1
- package/dist/sdk/index.js +35 -0
- package/dist/sdk/v1.generated.d.ts +141 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud.d.ts +12 -2
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +5171 -8535
- package/dist/server/openapi.d.ts +2714 -1937
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/v1.d.ts +2 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/config.d.ts +12 -30
- package/dist/storage/config.d.ts.map +1 -1
- package/dist/storage/index.d.ts +2 -2
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/interfaces.d.ts +13 -2
- package/dist/storage/interfaces.d.ts.map +1 -1
- package/dist/storage/local-sqlite.d.ts.map +1 -1
- package/dist/storage/postgres-adapter.d.ts.map +1 -1
- package/dist/storage/timestamp-backfill.d.ts +62 -0
- package/dist/storage/timestamp-backfill.d.ts.map +1 -0
- package/dist/storage.d.ts +2 -2
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +199 -63
- package/dist/task-subtree-transfer/authority.d.ts +27 -0
- package/dist/task-subtree-transfer/authority.d.ts.map +1 -0
- package/dist/task-subtree-transfer/backend.d.ts +9 -0
- package/dist/task-subtree-transfer/backend.d.ts.map +1 -0
- package/dist/task-subtree-transfer/canonical.d.ts +14 -0
- package/dist/task-subtree-transfer/canonical.d.ts.map +1 -0
- package/dist/task-subtree-transfer/http.d.ts +16 -0
- package/dist/task-subtree-transfer/http.d.ts.map +1 -0
- package/dist/task-subtree-transfer/index.d.ts +6 -0
- package/dist/task-subtree-transfer/index.d.ts.map +1 -0
- package/dist/task-subtree-transfer/planner.d.ts +24 -0
- package/dist/task-subtree-transfer/planner.d.ts.map +1 -0
- package/dist/task-subtree-transfer/postgres.d.ts +19 -0
- package/dist/task-subtree-transfer/postgres.d.ts.map +1 -0
- package/dist/task-subtree-transfer/schema-sql.d.ts +5 -0
- package/dist/task-subtree-transfer/schema-sql.d.ts.map +1 -0
- package/dist/task-subtree-transfer/sqlite.d.ts +16 -0
- package/dist/task-subtree-transfer/sqlite.d.ts.map +1 -0
- package/dist/task-subtree-transfer/types.d.ts +148 -0
- package/dist/task-subtree-transfer/types.d.ts.map +1 -0
- package/dist/task-subtree-transfer.d.ts +2 -0
- package/dist/task-subtree-transfer.d.ts.map +1 -0
- package/dist/task-subtree-transfer.js +1869 -0
- package/dist/testing.d.ts +41 -7
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +44 -1
- package/dist/types/index.d.ts +24 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +8 -4
package/dist/mcp/index.js
CHANGED
|
@@ -2888,6 +2888,20 @@ function ensureSchema(db) {
|
|
|
2888
2888
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_event ON webhook_deliveries(event)");
|
|
2889
2889
|
ensureColumn("task_comments", "type", "TEXT DEFAULT 'comment'");
|
|
2890
2890
|
ensureColumn("task_comments", "progress_pct", "INTEGER");
|
|
2891
|
+
ensureTable("plan_comments", `
|
|
2892
|
+
CREATE TABLE plan_comments (
|
|
2893
|
+
id TEXT PRIMARY KEY,
|
|
2894
|
+
plan_id TEXT NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
|
|
2895
|
+
agent_id TEXT,
|
|
2896
|
+
session_id TEXT,
|
|
2897
|
+
content TEXT NOT NULL,
|
|
2898
|
+
type TEXT DEFAULT 'comment' CHECK(type IN ('comment', 'progress', 'note')),
|
|
2899
|
+
progress_pct INTEGER CHECK(progress_pct IS NULL OR (progress_pct >= 0 AND progress_pct <= 100)),
|
|
2900
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
2901
|
+
machine_id TEXT,
|
|
2902
|
+
synced_at TEXT
|
|
2903
|
+
)`);
|
|
2904
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plan_comments_plan ON plan_comments(plan_id)");
|
|
2891
2905
|
ensureColumn("projects", "machine_id", "TEXT");
|
|
2892
2906
|
ensureColumn("projects", "synced_at", "TEXT");
|
|
2893
2907
|
ensureColumn("tasks", "machine_id", "TEXT");
|
|
@@ -3884,6 +3898,7 @@ var init_machines = __esm(() => {
|
|
|
3884
3898
|
"task_lists",
|
|
3885
3899
|
"plans",
|
|
3886
3900
|
"task_comments",
|
|
3901
|
+
"plan_comments",
|
|
3887
3902
|
"sessions",
|
|
3888
3903
|
"task_history",
|
|
3889
3904
|
"webhooks",
|
|
@@ -4112,20 +4127,16 @@ var init_sync_utils = __esm(() => {
|
|
|
4112
4127
|
// src/storage/config.ts
|
|
4113
4128
|
var exports_config = {};
|
|
4114
4129
|
__export(exports_config, {
|
|
4115
|
-
parseStorageMode: () => parseStorageMode,
|
|
4116
4130
|
parseStorageBackend: () => parseStorageBackend,
|
|
4117
4131
|
loadTodosStorageConfig: () => loadTodosStorageConfig,
|
|
4118
4132
|
loadStorageConfig: () => loadStorageConfig,
|
|
4119
4133
|
isTodosShadowEnabled: () => isTodosShadowEnabled,
|
|
4120
|
-
isTodosRemoteStorageEnabled: () => isTodosRemoteStorageEnabled,
|
|
4121
4134
|
isTodosPostgresBackend: () => isTodosPostgresBackend,
|
|
4122
4135
|
getTodosStorageShadowEnvName: () => getTodosStorageShadowEnvName,
|
|
4123
|
-
getTodosStorageMode: () => getTodosStorageMode,
|
|
4124
4136
|
getTodosStorageEnvName: () => getTodosStorageEnvName,
|
|
4125
4137
|
getTodosStorageDatabaseUrl: () => getTodosStorageDatabaseUrl,
|
|
4126
4138
|
getTodosStorageDatabaseEnv: () => getTodosStorageDatabaseEnv,
|
|
4127
4139
|
getTodosStorageBackend: () => getTodosStorageBackend,
|
|
4128
|
-
getStorageMode: () => getStorageMode,
|
|
4129
4140
|
getStorageDatabaseUrl: () => getStorageDatabaseUrl,
|
|
4130
4141
|
getStorageDatabaseEnv: () => getStorageDatabaseEnv,
|
|
4131
4142
|
getCanonicalTodosRdsConfig: () => getCanonicalTodosRdsConfig,
|
|
@@ -4135,7 +4146,6 @@ __export(exports_config, {
|
|
|
4135
4146
|
TODOS_STORAGE_FALLBACK_ENV: () => TODOS_STORAGE_FALLBACK_ENV,
|
|
4136
4147
|
TODOS_STORAGE_ENV: () => TODOS_STORAGE_ENV,
|
|
4137
4148
|
STORAGE_TABLES: () => STORAGE_TABLES,
|
|
4138
|
-
REMOVED_STORAGE_MODE_ENV_KEYS: () => REMOVED_STORAGE_MODE_ENV_KEYS,
|
|
4139
4149
|
CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV: () => CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV,
|
|
4140
4150
|
CANONICAL_TODOS_RDS_DATABASE: () => CANONICAL_TODOS_RDS_DATABASE,
|
|
4141
4151
|
CANONICAL_TODOS_RDS_CLUSTER_ENV: () => CANONICAL_TODOS_RDS_CLUSTER_ENV
|
|
@@ -4190,9 +4200,6 @@ function loadStorageConfig(env = process.env) {
|
|
|
4190
4200
|
function isTodosPostgresBackend(config) {
|
|
4191
4201
|
return config.mode === "postgres";
|
|
4192
4202
|
}
|
|
4193
|
-
function isTodosRemoteStorageEnabled(config) {
|
|
4194
|
-
return isTodosPostgresBackend(config);
|
|
4195
|
-
}
|
|
4196
4203
|
function assertTodosRemoteStorageConfig(config) {
|
|
4197
4204
|
if (!isTodosPostgresBackend(config))
|
|
4198
4205
|
return;
|
|
@@ -4208,32 +4215,15 @@ function parseStorageBackend(value) {
|
|
|
4208
4215
|
return "sqlite";
|
|
4209
4216
|
if (normalized === "postgres" || normalized === "postgresql")
|
|
4210
4217
|
return "postgres";
|
|
4211
|
-
if (["local", "remote", "cloud", "hybrid", "self_hosted"].includes(normalized)) {
|
|
4212
|
-
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} selects the backend. Deployment modes no longer exist: ` + `delete the storage-mode variable and set ${TODOS_STORAGE_ENV.databaseUrl} to select the ` + `postgresql backend, or leave it unset for sqlite.`);
|
|
4213
|
-
}
|
|
4214
4218
|
throw new Error(`Storage backend must be sqlite or postgres`);
|
|
4215
4219
|
}
|
|
4216
|
-
function parseStorageMode(value) {
|
|
4217
|
-
return parseStorageBackend(value);
|
|
4218
|
-
}
|
|
4219
4220
|
function getTodosStorageBackend(env = process.env) {
|
|
4220
|
-
for (const key of REMOVED_STORAGE_MODE_ENV_KEYS) {
|
|
4221
|
-
if (Object.hasOwn(env, key) && env[key] !== undefined) {
|
|
4222
|
-
throw new Error(`${key} was removed. Deployment modes no longer exist: delete the storage-mode variable. ` + `Set ${TODOS_STORAGE_ENV.databaseUrl} to select the postgresql backend, ` + `or leave it unset for sqlite.`);
|
|
4223
|
-
}
|
|
4224
|
-
}
|
|
4225
4221
|
if (!getTodosStorageDatabaseUrl(env))
|
|
4226
4222
|
return "sqlite";
|
|
4227
4223
|
if (isTodosShadowEnabled(env))
|
|
4228
4224
|
return "sqlite";
|
|
4229
4225
|
return "postgres";
|
|
4230
4226
|
}
|
|
4231
|
-
function getTodosStorageMode(env = process.env) {
|
|
4232
|
-
return getTodosStorageBackend(env);
|
|
4233
|
-
}
|
|
4234
|
-
function getStorageMode(env = process.env) {
|
|
4235
|
-
return getTodosStorageBackend(env);
|
|
4236
|
-
}
|
|
4237
4227
|
function isTodosShadowEnabled(env = process.env) {
|
|
4238
4228
|
return parseBoolean(readStorageEnv(env, "shadow").value, false);
|
|
4239
4229
|
}
|
|
@@ -4300,7 +4290,7 @@ function parsePositiveInteger(value, fallback) {
|
|
|
4300
4290
|
}
|
|
4301
4291
|
return parsed;
|
|
4302
4292
|
}
|
|
4303
|
-
var TODOS_STORAGE_TABLES, STORAGE_TABLES, TODOS_STORAGE_ENV, TODOS_STORAGE_FALLBACK_ENV,
|
|
4293
|
+
var TODOS_STORAGE_TABLES, STORAGE_TABLES, TODOS_STORAGE_ENV, TODOS_STORAGE_FALLBACK_ENV, CANONICAL_TODOS_RDS_CLUSTER_ENV = "HASNA_TODOS_RDS_CLUSTER", CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV = "HASNA_TODOS_RDS_RUNTIME_PATH", CANONICAL_TODOS_RDS_DATABASE = "todos";
|
|
4304
4294
|
var init_config = __esm(() => {
|
|
4305
4295
|
TODOS_STORAGE_TABLES = [
|
|
4306
4296
|
"todos_sync_records",
|
|
@@ -4339,12 +4329,6 @@ var init_config = __esm(() => {
|
|
|
4339
4329
|
syncBatchSize: "TODOS_SYNC_BATCH_SIZE",
|
|
4340
4330
|
syncDryRun: "TODOS_SYNC_DRY_RUN"
|
|
4341
4331
|
};
|
|
4342
|
-
REMOVED_STORAGE_MODE_ENV_KEYS = [
|
|
4343
|
-
"HASNA_TODOS_STORAGE_MODE",
|
|
4344
|
-
"HASNA_TODOS_MODE",
|
|
4345
|
-
"TODOS_STORAGE_MODE",
|
|
4346
|
-
"TODOS_MODE"
|
|
4347
|
-
];
|
|
4348
4332
|
});
|
|
4349
4333
|
|
|
4350
4334
|
// src/storage/shadow-outbox-schema.ts
|
|
@@ -11747,22 +11731,31 @@ function updateTaskList(id, input, db) {
|
|
|
11747
11731
|
throw new TaskListNotFoundError(id);
|
|
11748
11732
|
const sets = ["updated_at = ?"];
|
|
11749
11733
|
const params = [now()];
|
|
11750
|
-
|
|
11751
|
-
|
|
11752
|
-
|
|
11753
|
-
|
|
11754
|
-
|
|
11734
|
+
const projectId = input.project_id === "" || input.project_id === null ? null : input.project_id === undefined ? existing.project_id : input.project_id;
|
|
11735
|
+
const scopeChanged = projectId !== existing.project_id;
|
|
11736
|
+
const effectiveSlug = input.slug !== undefined ? slugify(input.slug) : existing.slug;
|
|
11737
|
+
if (input.slug !== undefined && !effectiveSlug) {
|
|
11738
|
+
throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
|
|
11739
|
+
}
|
|
11740
|
+
if (scopeChanged || input.slug !== undefined && effectiveSlug !== existing.slug) {
|
|
11741
|
+
const duplicate = projectId ? d.query("SELECT id FROM task_lists WHERE project_id = ? AND slug = ? AND id != ?").get(projectId, effectiveSlug, id) : d.query("SELECT id FROM task_lists WHERE project_id IS NULL AND slug = ? AND id != ?").get(effectiveSlug, id);
|
|
11755
11742
|
if (duplicate) {
|
|
11756
|
-
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${
|
|
11743
|
+
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${effectiveSlug}" already exists in this scope`);
|
|
11757
11744
|
}
|
|
11758
|
-
if (
|
|
11745
|
+
if (scopeChanged || effectiveSlug !== existing.slug) {
|
|
11759
11746
|
releaseCanonicalSlugClaims("task_list", id, d);
|
|
11760
|
-
if (!claimCanonicalSlug("task_list", taskListSlugScopeKey(
|
|
11761
|
-
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${
|
|
11747
|
+
if (!claimCanonicalSlug("task_list", taskListSlugScopeKey(projectId), effectiveSlug, id, d)) {
|
|
11748
|
+
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${effectiveSlug}" already exists in this scope`);
|
|
11762
11749
|
}
|
|
11763
11750
|
}
|
|
11751
|
+
}
|
|
11752
|
+
if (input.slug !== undefined) {
|
|
11764
11753
|
sets.push("slug = ?");
|
|
11765
|
-
params.push(
|
|
11754
|
+
params.push(effectiveSlug);
|
|
11755
|
+
}
|
|
11756
|
+
if (projectId !== existing.project_id) {
|
|
11757
|
+
sets.push("project_id = ?");
|
|
11758
|
+
params.push(projectId);
|
|
11766
11759
|
}
|
|
11767
11760
|
if (input.name !== undefined) {
|
|
11768
11761
|
sets.push("name = ?");
|
|
@@ -13959,8 +13952,8 @@ function failTask(id, agentId, reason, options, db) {
|
|
|
13959
13952
|
const safeMeta = sanitizePreWriteValue(meta, "task.failure.metadata");
|
|
13960
13953
|
const timestamp2 = now();
|
|
13961
13954
|
const failTx = d.transaction(() => {
|
|
13962
|
-
const res = d.run(`UPDATE tasks SET status = 'failed', reason = ?, locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
|
|
13963
|
-
WHERE id = ? AND version = ?`, [safeReason, JSON.stringify(safeMeta), timestamp2, id, task.version]);
|
|
13955
|
+
const res = d.run(`UPDATE tasks SET status = 'failed', reason = ?, locked_by = NULL, locked_at = NULL, completed_at = COALESCE(completed_at, ?), metadata = ?, version = version + 1, updated_at = ?
|
|
13956
|
+
WHERE id = ? AND version = ?`, [safeReason, timestamp2, JSON.stringify(safeMeta), timestamp2, id, task.version]);
|
|
13964
13957
|
if (res.changes === 0) {
|
|
13965
13958
|
const current = getTask(id, d);
|
|
13966
13959
|
throw new VersionConflictError(id, task.version, current?.version ?? -1);
|
|
@@ -13972,6 +13965,7 @@ function failTask(id, agentId, reason, options, db) {
|
|
|
13972
13965
|
status: "failed",
|
|
13973
13966
|
locked_by: null,
|
|
13974
13967
|
locked_at: null,
|
|
13968
|
+
completed_at: task.completed_at ?? timestamp2,
|
|
13975
13969
|
reason: safeReason,
|
|
13976
13970
|
metadata: safeMeta,
|
|
13977
13971
|
version: task.version + 1,
|
|
@@ -14620,13 +14614,20 @@ function updateTaskStored(id, input, db) {
|
|
|
14620
14614
|
if (isTerminalStatus(input.status)) {
|
|
14621
14615
|
sets.push("locked_by = NULL");
|
|
14622
14616
|
sets.push("locked_at = NULL");
|
|
14623
|
-
|
|
14624
|
-
|
|
14625
|
-
|
|
14626
|
-
|
|
14617
|
+
if (input.status === "completed") {
|
|
14618
|
+
sets.push("completed_at = ?");
|
|
14619
|
+
params.push(completionTimestamp);
|
|
14620
|
+
} else {
|
|
14621
|
+
sets.push("completed_at = COALESCE(completed_at, ?)");
|
|
14622
|
+
params.push(completionTimestamp);
|
|
14623
|
+
}
|
|
14627
14624
|
} else if (task.status === "completed" && input.completed_at === undefined) {
|
|
14628
14625
|
sets.push("completed_at = NULL");
|
|
14629
14626
|
}
|
|
14627
|
+
if (input.status === "in_progress") {
|
|
14628
|
+
sets.push("started_at = COALESCE(started_at, ?)");
|
|
14629
|
+
params.push(timestamp2);
|
|
14630
|
+
}
|
|
14630
14631
|
}
|
|
14631
14632
|
if (input.priority !== undefined) {
|
|
14632
14633
|
sets.push("priority = ?");
|
|
@@ -14764,7 +14765,6 @@ function updateTaskStored(id, input, db) {
|
|
|
14764
14765
|
if (input.approved_by !== undefined)
|
|
14765
14766
|
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
14766
14767
|
const reopened = input.status !== undefined && input.status !== "completed" && task.status === "completed" && input.completed_at === undefined;
|
|
14767
|
-
const completedNow = input.status === "completed";
|
|
14768
14768
|
const terminalNow = input.status !== undefined && isTerminalStatus(input.status);
|
|
14769
14769
|
const updatedTask = {
|
|
14770
14770
|
...task,
|
|
@@ -14775,7 +14775,8 @@ function updateTaskStored(id, input, db) {
|
|
|
14775
14775
|
updated_at: timestamp2,
|
|
14776
14776
|
locked_by: terminalNow ? null : task.locked_by,
|
|
14777
14777
|
locked_at: terminalNow ? null : task.locked_at,
|
|
14778
|
-
completed_at:
|
|
14778
|
+
completed_at: input.status === "completed" ? completionTimestamp : input.completed_at !== undefined ? input.completed_at : terminalNow ? task.completed_at ?? completionTimestamp : reopened ? null : task.completed_at,
|
|
14779
|
+
started_at: input.status === "in_progress" ? task.started_at ?? timestamp2 : task.started_at,
|
|
14779
14780
|
sla_minutes: input.sla_minutes !== undefined ? input.sla_minutes : task.sla_minutes,
|
|
14780
14781
|
actual_minutes: input.actual_minutes ?? task.actual_minutes,
|
|
14781
14782
|
confidence: input.confidence !== undefined ? input.confidence : task.confidence,
|
|
@@ -14836,6 +14837,9 @@ function deleteTask(id, db) {
|
|
|
14836
14837
|
version: row.version
|
|
14837
14838
|
}, d);
|
|
14838
14839
|
const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
|
|
14840
|
+
if (result.changes > 0) {
|
|
14841
|
+
d.run("DELETE FROM task_dependencies WHERE task_id = ? OR depends_on = ?", [id, id]);
|
|
14842
|
+
}
|
|
14839
14843
|
return result.changes > 0;
|
|
14840
14844
|
}
|
|
14841
14845
|
var init_task_crud = __esm(() => {
|
|
@@ -15100,8 +15104,10 @@ function bulkDeleteTasks(taskIds, force = false, db) {
|
|
|
15100
15104
|
continue;
|
|
15101
15105
|
}
|
|
15102
15106
|
const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
|
|
15103
|
-
if (result.changes > 0)
|
|
15107
|
+
if (result.changes > 0) {
|
|
15108
|
+
d.run("DELETE FROM task_dependencies WHERE task_id = ? OR depends_on = ?", [id, id]);
|
|
15104
15109
|
deleted++;
|
|
15110
|
+
}
|
|
15105
15111
|
} catch (e) {
|
|
15106
15112
|
failed.push({ id, error: e instanceof Error ? e.message : String(e) });
|
|
15107
15113
|
}
|
|
@@ -21187,20 +21193,25 @@ var init_page_validation = __esm(() => {
|
|
|
21187
21193
|
import { resolveStorageClient } from "@hasna/contracts/client/storage";
|
|
21188
21194
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
21189
21195
|
import { resolve as resolvePath } from "path";
|
|
21190
|
-
function
|
|
21191
|
-
|
|
21192
|
-
|
|
21193
|
-
|
|
21194
|
-
|
|
21195
|
-
|
|
21196
|
+
function emitTodosLocalFallbackNotice(env) {
|
|
21197
|
+
if (todosLocalFallbackNoticeEmitted)
|
|
21198
|
+
return;
|
|
21199
|
+
todosLocalFallbackNoticeEmitted = true;
|
|
21200
|
+
const notice = {
|
|
21201
|
+
event: "todos-local-fallback",
|
|
21202
|
+
transport: "sqlite",
|
|
21203
|
+
source: "default",
|
|
21204
|
+
apiUrlPresent: Boolean(env.HASNA_TODOS_API_URL?.trim()),
|
|
21205
|
+
apiKeyPresent: Boolean(env.HASNA_TODOS_API_KEY?.trim()),
|
|
21206
|
+
notice: "No hosted API config (HASNA_TODOS_API_URL + HASNA_TODOS_API_KEY) is present; " + "using local SQLite. Hosted todos are NOT visible in this output."
|
|
21207
|
+
};
|
|
21208
|
+
console.error(JSON.stringify(notice));
|
|
21196
21209
|
}
|
|
21197
|
-
function
|
|
21198
|
-
assertNoLegacyStorageMode(env);
|
|
21210
|
+
function resolveTodosCliTransport(env = process.env) {
|
|
21199
21211
|
const urlValue = env.HASNA_TODOS_API_URL?.trim();
|
|
21200
21212
|
const keyValue = env.HASNA_TODOS_API_KEY?.trim();
|
|
21201
21213
|
if (urlValue && keyValue) {
|
|
21202
21214
|
return {
|
|
21203
|
-
mode: "http",
|
|
21204
21215
|
transport: "http",
|
|
21205
21216
|
selected: true,
|
|
21206
21217
|
source: "HASNA_TODOS_API_URL+HASNA_TODOS_API_KEY"
|
|
@@ -21212,15 +21223,15 @@ function resolveTodosCliStorageMode(env = process.env) {
|
|
|
21212
21223
|
if (keyValue) {
|
|
21213
21224
|
throw new Error("REMOTE_API_URL_MISSING: remote Todos storage requires HASNA_TODOS_API_URL; local SQLite fallback is disabled");
|
|
21214
21225
|
}
|
|
21226
|
+
emitTodosLocalFallbackNotice(env);
|
|
21215
21227
|
return {
|
|
21216
|
-
mode: "sqlite",
|
|
21217
21228
|
transport: "sqlite",
|
|
21218
21229
|
selected: false,
|
|
21219
21230
|
source: "default"
|
|
21220
21231
|
};
|
|
21221
21232
|
}
|
|
21222
21233
|
function requestedTransport(env) {
|
|
21223
|
-
return
|
|
21234
|
+
return resolveTodosCliTransport(env).transport;
|
|
21224
21235
|
}
|
|
21225
21236
|
function normalizeRemoteAuthorityUrl(value) {
|
|
21226
21237
|
const trimmed = value?.trim();
|
|
@@ -21254,13 +21265,13 @@ function normalizeRemoteAuthorityUrl(value) {
|
|
|
21254
21265
|
function getTodosRemoteAuthorityConfigStatus(env = process.env) {
|
|
21255
21266
|
let resolution;
|
|
21256
21267
|
try {
|
|
21257
|
-
resolution =
|
|
21268
|
+
resolution = resolveTodosCliTransport(env);
|
|
21258
21269
|
} catch (error) {
|
|
21259
21270
|
const issue = error instanceof Error ? error.message : String(error);
|
|
21260
21271
|
return {
|
|
21261
21272
|
selected: true,
|
|
21262
21273
|
ok: false,
|
|
21263
|
-
|
|
21274
|
+
transport: "invalid",
|
|
21264
21275
|
api_url_configured: Boolean(env.HASNA_TODOS_API_URL?.trim()),
|
|
21265
21276
|
api_key_configured: Boolean(env.HASNA_TODOS_API_KEY?.trim()),
|
|
21266
21277
|
v1_base_url: null,
|
|
@@ -21268,12 +21279,12 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env) {
|
|
|
21268
21279
|
local_fallback: false
|
|
21269
21280
|
};
|
|
21270
21281
|
}
|
|
21271
|
-
const {
|
|
21282
|
+
const { transport, selected } = resolution;
|
|
21272
21283
|
if (!selected) {
|
|
21273
21284
|
return {
|
|
21274
21285
|
selected: false,
|
|
21275
21286
|
ok: true,
|
|
21276
|
-
|
|
21287
|
+
transport,
|
|
21277
21288
|
api_url_configured: false,
|
|
21278
21289
|
api_key_configured: false,
|
|
21279
21290
|
v1_base_url: null,
|
|
@@ -21298,7 +21309,7 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env) {
|
|
|
21298
21309
|
return {
|
|
21299
21310
|
selected: true,
|
|
21300
21311
|
ok: issues.length === 0,
|
|
21301
|
-
|
|
21312
|
+
transport,
|
|
21302
21313
|
api_url_configured: apiUrl !== null,
|
|
21303
21314
|
api_key_configured: apiKeyConfigured,
|
|
21304
21315
|
v1_base_url: apiUrl ? `${apiUrl}/v1` : null,
|
|
@@ -21342,7 +21353,8 @@ function classifyRemoteRequestError(baseUrl, route, error) {
|
|
|
21342
21353
|
}
|
|
21343
21354
|
const message = error instanceof Error ? error.message : String(error);
|
|
21344
21355
|
if (error instanceof Error && error.name === "AbortError" || /abort|timed?\s*out/i.test(message)) {
|
|
21345
|
-
|
|
21356
|
+
const unboundedNote = route === "/tasks" ? "; this is likely an UNBOUNDED task read timing out (bounded task reads from this authority return in ~1 s) \u2014 retry with a bounded/count-specific shape such as todos list --limit; local SQLite fallback is disabled" : "; local SQLite fallback is disabled";
|
|
21357
|
+
throw new Error(`REMOTE_API_TIMEOUT: configured Todos authority ${baseUrl} timed out for ${route}${unboundedNote}`, { cause: error });
|
|
21346
21358
|
}
|
|
21347
21359
|
if (status === undefined) {
|
|
21348
21360
|
throw new Error(`REMOTE_API_UNREACHABLE: configured Todos authority ${baseUrl} could not be reached for ${route}; ` + "local SQLite fallback is disabled", { cause: error });
|
|
@@ -21429,11 +21441,10 @@ function getTodosCloudClient(env = process.env, requestTimeoutMs = REMOTE_REQUES
|
|
|
21429
21441
|
fetchImpl: (input, init) => globalThis.fetch(input, { ...init, redirect: "manual" }),
|
|
21430
21442
|
timeoutMs: requestTimeoutMs
|
|
21431
21443
|
});
|
|
21432
|
-
if (resolved.transport === "cloud-http")
|
|
21433
|
-
return protectRemoteClient(resolved.client, requestTimeoutMs);
|
|
21434
21444
|
const transportName = resolved.transport;
|
|
21435
|
-
if (transportName === "http")
|
|
21445
|
+
if (transportName === "cloud-http" || transportName === "http") {
|
|
21436
21446
|
return protectRemoteClient(resolved.client, requestTimeoutMs);
|
|
21447
|
+
}
|
|
21437
21448
|
return null;
|
|
21438
21449
|
}
|
|
21439
21450
|
function unwrapTask(raw) {
|
|
@@ -21905,7 +21916,7 @@ async function cloudResolveTaskListRef(client, ref, projectId) {
|
|
|
21905
21916
|
return input.toLowerCase();
|
|
21906
21917
|
return (await cloudResolveTaskList(client, ref, projectId)).id;
|
|
21907
21918
|
}
|
|
21908
|
-
var UUID_RE, completionCapabilityCache, retryCapabilityCache, taskCreatorCapabilityCache, gitRefCapabilityCache, remoteCommandCapabilityCache,
|
|
21919
|
+
var UUID_RE, completionCapabilityCache, retryCapabilityCache, taskCreatorCapabilityCache, gitRefCapabilityCache, remoteCommandCapabilityCache, todosLocalFallbackNoticeEmitted = false, REMOTE_REQUEST_TIMEOUT_MS = 1e4, PRIORITY_RANK, listTagsCapabilityCache;
|
|
21909
21920
|
var init_cloud_router = __esm(() => {
|
|
21910
21921
|
init_types();
|
|
21911
21922
|
init_redaction();
|
|
@@ -21919,12 +21930,6 @@ var init_cloud_router = __esm(() => {
|
|
|
21919
21930
|
taskCreatorCapabilityCache = new Map;
|
|
21920
21931
|
gitRefCapabilityCache = new Map;
|
|
21921
21932
|
remoteCommandCapabilityCache = new Map;
|
|
21922
|
-
LEGACY_STORAGE_MODE_KEYS = [
|
|
21923
|
-
"HASNA_TODOS_STORAGE_MODE",
|
|
21924
|
-
"HASNA_TODOS_MODE",
|
|
21925
|
-
"TODOS_STORAGE_MODE",
|
|
21926
|
-
"TODOS_MODE"
|
|
21927
|
-
];
|
|
21928
21933
|
PRIORITY_RANK = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
21929
21934
|
listTagsCapabilityCache = new Map;
|
|
21930
21935
|
});
|
|
@@ -36042,7 +36047,7 @@ var package_default;
|
|
|
36042
36047
|
var init_package = __esm(() => {
|
|
36043
36048
|
package_default = {
|
|
36044
36049
|
name: "@hasna/todos",
|
|
36045
|
-
version: "0.15.
|
|
36050
|
+
version: "0.15.41",
|
|
36046
36051
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
36047
36052
|
type: "module",
|
|
36048
36053
|
main: "dist/index.js",
|
|
@@ -36088,6 +36093,10 @@ var init_package = __esm(() => {
|
|
|
36088
36093
|
"./task-manifest": {
|
|
36089
36094
|
types: "./dist/task-manifest.d.ts",
|
|
36090
36095
|
import: "./dist/task-manifest.js"
|
|
36096
|
+
},
|
|
36097
|
+
"./task-subtree-transfer": {
|
|
36098
|
+
types: "./dist/task-subtree-transfer.d.ts",
|
|
36099
|
+
import: "./dist/task-subtree-transfer.js"
|
|
36091
36100
|
}
|
|
36092
36101
|
},
|
|
36093
36102
|
workspaces: [
|
|
@@ -36101,8 +36110,8 @@ var init_package = __esm(() => {
|
|
|
36101
36110
|
"README.md"
|
|
36102
36111
|
],
|
|
36103
36112
|
scripts: {
|
|
36104
|
-
build: "rm -rf dist dashboard/dist && cd dashboard && bun install --frozen-lockfile && bun run build && cd .. && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts src/task-manifest.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
|
|
36105
|
-
"build:server": "rm -rf dist && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts src/task-manifest.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*'",
|
|
36113
|
+
build: "rm -rf dist dashboard/dist && cd dashboard && bun install --frozen-lockfile && bun run build && cd .. && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts src/task-manifest.ts src/task-subtree-transfer.ts --outdir dist --root src --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
|
|
36114
|
+
"build:server": "rm -rf dist && bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/server/index.ts --outdir dist/server --target bun && bun build src/sdk/index.ts --outdir dist/sdk --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && bun build src/index.ts src/mcp.ts src/registry.ts src/contracts.ts src/storage.ts src/testing.ts src/project-registration.ts src/task-manifest.ts src/task-subtree-transfer.ts --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*'",
|
|
36106
36115
|
migrate: "bun run src/server/index.ts migrate",
|
|
36107
36116
|
"backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
|
|
36108
36117
|
"generate:sdk": "bun run scripts/generate-sdk.ts",
|
|
@@ -36156,7 +36165,7 @@ var init_package = __esm(() => {
|
|
|
36156
36165
|
author: "Andrei Hasna <andrei@hasna.com>",
|
|
36157
36166
|
license: "Apache-2.0",
|
|
36158
36167
|
dependencies: {
|
|
36159
|
-
"@hasna/contracts": "0.
|
|
36168
|
+
"@hasna/contracts": "0.13.3",
|
|
36160
36169
|
"@hasna/events": "^0.1.11",
|
|
36161
36170
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
36162
36171
|
chalk: "^5.4.1",
|
|
@@ -45433,6 +45442,75 @@ var init_cloud_client = __esm(() => {
|
|
|
45433
45442
|
init_config();
|
|
45434
45443
|
});
|
|
45435
45444
|
|
|
45445
|
+
// src/db/plan-comments.ts
|
|
45446
|
+
function addPlanComment(input, db) {
|
|
45447
|
+
const d = db || getDatabase();
|
|
45448
|
+
if (!getPlan(input.plan_id, d)) {
|
|
45449
|
+
throw new PlanNotFoundError(input.plan_id);
|
|
45450
|
+
}
|
|
45451
|
+
const id = uuid();
|
|
45452
|
+
const timestamp4 = now();
|
|
45453
|
+
d.run(`INSERT INTO plan_comments (id, plan_id, agent_id, session_id, content, type, progress_pct, created_at)
|
|
45454
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
45455
|
+
id,
|
|
45456
|
+
input.plan_id,
|
|
45457
|
+
input.agent_id || null,
|
|
45458
|
+
input.session_id || null,
|
|
45459
|
+
sanitizePreWriteText(input.content, "comment.content"),
|
|
45460
|
+
input.type || "comment",
|
|
45461
|
+
input.progress_pct ?? null,
|
|
45462
|
+
timestamp4
|
|
45463
|
+
]);
|
|
45464
|
+
return getPlanComment(id, d);
|
|
45465
|
+
}
|
|
45466
|
+
function getPlanComment(id, db) {
|
|
45467
|
+
const d = db || getDatabase();
|
|
45468
|
+
return d.query("SELECT * FROM plan_comments WHERE id = ?").get(id);
|
|
45469
|
+
}
|
|
45470
|
+
function listPlanComments(planId, db) {
|
|
45471
|
+
const d = db || getDatabase();
|
|
45472
|
+
return d.query("SELECT * FROM plan_comments WHERE plan_id = ? ORDER BY created_at, rowid").all(planId);
|
|
45473
|
+
}
|
|
45474
|
+
var init_plan_comments = __esm(() => {
|
|
45475
|
+
init_types();
|
|
45476
|
+
init_database();
|
|
45477
|
+
init_plans();
|
|
45478
|
+
init_prewrite_secrets();
|
|
45479
|
+
});
|
|
45480
|
+
|
|
45481
|
+
// src/lib/comment-cursor.ts
|
|
45482
|
+
function encodeCommentCursor(comment) {
|
|
45483
|
+
return Buffer.from(JSON.stringify({ created_at: comment.created_at, id: comment.id }), "utf8").toString("base64url");
|
|
45484
|
+
}
|
|
45485
|
+
function decodeCommentCursor(value) {
|
|
45486
|
+
if (value.length > MAX_COMMENT_CURSOR_LENGTH)
|
|
45487
|
+
throw new Error("invalid comment cursor");
|
|
45488
|
+
let parsed;
|
|
45489
|
+
try {
|
|
45490
|
+
parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
|
|
45491
|
+
} catch {
|
|
45492
|
+
throw new Error("invalid comment cursor");
|
|
45493
|
+
}
|
|
45494
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
45495
|
+
throw new Error("invalid comment cursor");
|
|
45496
|
+
const cursor = parsed;
|
|
45497
|
+
if (typeof cursor["created_at"] !== "string" || cursor["created_at"].length > 64 || !Number.isFinite(Date.parse(cursor["created_at"])) || typeof cursor["id"] !== "string" || !cursor["id"] || cursor["id"].length > 256) {
|
|
45498
|
+
throw new Error("invalid comment cursor");
|
|
45499
|
+
}
|
|
45500
|
+
return { created_at: cursor["created_at"], id: cursor["id"] };
|
|
45501
|
+
}
|
|
45502
|
+
function isStrictlyOlder(comment, before) {
|
|
45503
|
+
return comment.created_at < before.created_at || comment.created_at === before.created_at && comment.id < before.id;
|
|
45504
|
+
}
|
|
45505
|
+
function compareCommentKeyset(left, right) {
|
|
45506
|
+
if (isStrictlyOlder(left, right))
|
|
45507
|
+
return -1;
|
|
45508
|
+
if (isStrictlyOlder(right, left))
|
|
45509
|
+
return 1;
|
|
45510
|
+
return 0;
|
|
45511
|
+
}
|
|
45512
|
+
var MAX_COMMENT_CURSOR_LENGTH = 1024;
|
|
45513
|
+
|
|
45436
45514
|
// src/db/plan-project-links.ts
|
|
45437
45515
|
function getPlanProjectLinkReceipt(receiptId, db) {
|
|
45438
45516
|
const d = db || getDatabase();
|
|
@@ -46239,7 +46317,22 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
46239
46317
|
list: (projectId) => listPlans(projectId, database()),
|
|
46240
46318
|
update: (id, input) => updatePlan(id, input, database()),
|
|
46241
46319
|
completeAtRevision: (id, expectedUpdatedAt) => completePlanAtRevision(id, expectedUpdatedAt, database()),
|
|
46242
|
-
delete: (id) => deletePlan(id, database())
|
|
46320
|
+
delete: (id) => deletePlan(id, database()),
|
|
46321
|
+
addComment: (input) => addPlanComment(input, database()),
|
|
46322
|
+
getComments: (planId) => listPlanComments(planId, database()),
|
|
46323
|
+
getCommentsPage: (planId, options2) => {
|
|
46324
|
+
if (options2?.limit !== undefined && (!Number.isSafeInteger(options2.limit) || options2.limit < 1 || options2.limit > 1001)) {
|
|
46325
|
+
throw new Error("Plan comment limit must be an integer between 1 and 1001");
|
|
46326
|
+
}
|
|
46327
|
+
let comments = listPlanComments(planId, database());
|
|
46328
|
+
comments = comments.sort(compareCommentKeyset);
|
|
46329
|
+
if (options2?.before) {
|
|
46330
|
+
const before = options2.before;
|
|
46331
|
+
comments = comments.filter((comment) => isStrictlyOlder(comment, before));
|
|
46332
|
+
}
|
|
46333
|
+
const limit = options2?.limit ?? 100;
|
|
46334
|
+
return comments.slice(-limit);
|
|
46335
|
+
}
|
|
46243
46336
|
},
|
|
46244
46337
|
planProjectLinks: {
|
|
46245
46338
|
apply: (input) => applyPlanProjectLinkSqlite(input, database()),
|
|
@@ -46280,10 +46373,10 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
46280
46373
|
throw new Error("Comment limit must be an integer between 1 and 1001");
|
|
46281
46374
|
}
|
|
46282
46375
|
let comments = listComments(taskId, database());
|
|
46283
|
-
comments = comments.sort(
|
|
46376
|
+
comments = comments.sort(compareCommentKeyset);
|
|
46284
46377
|
if (options2?.before) {
|
|
46285
46378
|
const before = options2.before;
|
|
46286
|
-
comments = comments.filter((comment) => comment
|
|
46379
|
+
comments = comments.filter((comment) => isStrictlyOlder(comment, before));
|
|
46287
46380
|
}
|
|
46288
46381
|
if (options2?.limit !== undefined)
|
|
46289
46382
|
comments = comments.slice(-options2.limit);
|
|
@@ -46314,6 +46407,7 @@ var init_local_sqlite = __esm(() => {
|
|
|
46314
46407
|
init_tasks();
|
|
46315
46408
|
init_projects();
|
|
46316
46409
|
init_plans();
|
|
46410
|
+
init_plan_comments();
|
|
46317
46411
|
init_agents();
|
|
46318
46412
|
init_task_lists();
|
|
46319
46413
|
init_templates();
|
|
@@ -47226,7 +47320,26 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
47226
47320
|
list: async (projectId) => (await store.list("plans")).filter((plan) => projectId === undefined || plan.project_id === projectId).sort((a, b) => a.name.localeCompare(b.name)),
|
|
47227
47321
|
update: (id, input) => updatePlan2(id, input, store),
|
|
47228
47322
|
completeAtRevision: (id, expectedUpdatedAt, context) => store.completePlanAtRevision(id, expectedUpdatedAt, context),
|
|
47229
|
-
delete: (id, context) => store.deletePlan(id, context)
|
|
47323
|
+
delete: (id, context) => store.deletePlan(id, context),
|
|
47324
|
+
addComment: (input, context) => addPlanComment2(input, store, context),
|
|
47325
|
+
getComments: async (planId) => {
|
|
47326
|
+
const pages = [];
|
|
47327
|
+
let before;
|
|
47328
|
+
while (true) {
|
|
47329
|
+
const page = await store.listPlanComments(planId, { limit: 1000, ...before ? { before } : {} });
|
|
47330
|
+
if (page.length === 0)
|
|
47331
|
+
break;
|
|
47332
|
+
pages.unshift(page);
|
|
47333
|
+
if (page.length < 1000)
|
|
47334
|
+
break;
|
|
47335
|
+
const oldest = page[0];
|
|
47336
|
+
before = { created_at: oldest.created_at, id: oldest.id };
|
|
47337
|
+
}
|
|
47338
|
+
return pages.flat().map(redactPlanComment).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
|
|
47339
|
+
},
|
|
47340
|
+
getCommentsPage: async (planId, options2) => {
|
|
47341
|
+
return (await store.listPlanComments(planId, options2)).map(redactPlanComment).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
|
|
47342
|
+
}
|
|
47230
47343
|
},
|
|
47231
47344
|
planProjectLinks: {
|
|
47232
47345
|
apply: (input, context) => store.applyPlanProjectLink(input, context),
|
|
@@ -47444,6 +47557,27 @@ class PostgresJsonRecordStore {
|
|
|
47444
47557
|
LIMIT $${params.length}`, params);
|
|
47445
47558
|
return result.rows.map((row) => payloadRecord2(row.payload)).reverse();
|
|
47446
47559
|
}
|
|
47560
|
+
async listPlanComments(planId, options = {}) {
|
|
47561
|
+
await this.ensureSchema();
|
|
47562
|
+
const limit = options.limit ?? 100;
|
|
47563
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1001) {
|
|
47564
|
+
throw new Error("Postgres plan comment limit must be an integer between 1 and 1001");
|
|
47565
|
+
}
|
|
47566
|
+
const params = [this.service, planId];
|
|
47567
|
+
let cursorPredicate = "";
|
|
47568
|
+
if (options.before) {
|
|
47569
|
+
params.push(options.before.created_at, options.before.id);
|
|
47570
|
+
cursorPredicate = `AND (payload->>'created_at', object_id) < ($3, $4)`;
|
|
47571
|
+
}
|
|
47572
|
+
params.push(limit);
|
|
47573
|
+
const result = await this.options.client.query(`/* todos:list-plan-comments */ SELECT payload FROM ${this.tableName}
|
|
47574
|
+
WHERE service = $1 AND object_type = 'plan_comments' AND deleted_at IS NULL
|
|
47575
|
+
AND payload->>'plan_id' = $2
|
|
47576
|
+
${cursorPredicate}
|
|
47577
|
+
ORDER BY payload->>'created_at' DESC, object_id DESC
|
|
47578
|
+
LIMIT $${params.length}`, params);
|
|
47579
|
+
return result.rows.map((row) => payloadRecord2(row.payload)).reverse();
|
|
47580
|
+
}
|
|
47447
47581
|
async listRecords(type) {
|
|
47448
47582
|
await this.ensureSchema();
|
|
47449
47583
|
const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at
|
|
@@ -48605,6 +48739,15 @@ class PostgresJsonRecordStore {
|
|
|
48605
48739
|
WHERE r.service = $1 AND r.object_type = 'tasks' AND r.deleted_at IS NULL
|
|
48606
48740
|
AND r.payload->>'plan_id' = $2 AND EXISTS (SELECT 1 FROM target)
|
|
48607
48741
|
RETURNING 1
|
|
48742
|
+
), deleted_plan_comments AS (
|
|
48743
|
+
UPDATE ${this.tableName} r SET
|
|
48744
|
+
deleted_at = $3::timestamptz,
|
|
48745
|
+
updated_at = $3::timestamptz,
|
|
48746
|
+
source_machine_id = COALESCE($4, r.source_machine_id),
|
|
48747
|
+
version = COALESCE(r.version, 0) + 1
|
|
48748
|
+
WHERE r.service = $1 AND r.object_type = 'plan_comments' AND r.deleted_at IS NULL
|
|
48749
|
+
AND r.payload->>'plan_id' = $2 AND EXISTS (SELECT 1 FROM target)
|
|
48750
|
+
RETURNING 1
|
|
48608
48751
|
), deleted_plan AS (
|
|
48609
48752
|
UPDATE ${this.tableName} r SET
|
|
48610
48753
|
deleted_at = $3::timestamptz,
|
|
@@ -48780,7 +48923,8 @@ async function updateTask2(id, input, store, context) {
|
|
|
48780
48923
|
task_list_id: input.task_list_id !== undefined ? input.task_list_id : existing.task_list_id,
|
|
48781
48924
|
parent_id: input.parent_id !== undefined ? input.parent_id : existing.parent_id,
|
|
48782
48925
|
created_by: existing.created_by,
|
|
48783
|
-
completed_at:
|
|
48926
|
+
completed_at: input.status === "completed" ? input.completed_at ?? new Date().toISOString() : input.completed_at !== undefined ? input.completed_at : input.status !== undefined && isTerminalStatus(input.status) ? existing.completed_at ?? new Date().toISOString() : reopened ? null : existing.completed_at,
|
|
48927
|
+
started_at: input.status === "in_progress" ? existing.started_at ?? new Date().toISOString() : existing.started_at
|
|
48784
48928
|
};
|
|
48785
48929
|
const storedTask = await store.upsertTaskWithPlanMembershipGuard(task2, [existing.plan_id, effectivePlanId].filter((planId) => Boolean(planId)), input.project_id !== undefined, context, {
|
|
48786
48930
|
operation: "update",
|
|
@@ -48825,7 +48969,8 @@ async function failTask2(id, agentId, reason, options, store) {
|
|
|
48825
48969
|
status: "failed",
|
|
48826
48970
|
assigned_to: task2.assigned_to ?? agentId ?? null,
|
|
48827
48971
|
reason: reason ?? task2.reason,
|
|
48828
|
-
retry_after: options?.retry_after ?? task2.retry_after
|
|
48972
|
+
retry_after: options?.retry_after ?? task2.retry_after,
|
|
48973
|
+
completed_at: task2.completed_at ?? new Date().toISOString()
|
|
48829
48974
|
}, store);
|
|
48830
48975
|
if (!options?.retry)
|
|
48831
48976
|
return { task: failed };
|
|
@@ -49270,16 +49415,22 @@ async function createTaskList2(input, store, context) {
|
|
|
49270
49415
|
async function updateTaskList2(id, input, store) {
|
|
49271
49416
|
const list = await requireRecord("task_lists", id, store);
|
|
49272
49417
|
const patch = definedPatch(input);
|
|
49418
|
+
const projectId = input.project_id === "" || input.project_id === null ? null : input.project_id === undefined ? list.project_id : input.project_id;
|
|
49273
49419
|
if (input.slug !== undefined) {
|
|
49274
49420
|
const slug = slugifyRaw(input.slug);
|
|
49275
49421
|
if (!slug)
|
|
49276
49422
|
throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
|
|
49277
|
-
|
|
49423
|
+
patch.slug = slug;
|
|
49424
|
+
}
|
|
49425
|
+
const effectiveSlug = patch.slug ?? list.slug;
|
|
49426
|
+
if (projectId !== list.project_id || effectiveSlug !== list.slug) {
|
|
49427
|
+
const duplicate = (await store.list("task_lists")).find((candidate) => candidate.id !== id && candidate.project_id === projectId && candidate.slug === effectiveSlug);
|
|
49278
49428
|
if (duplicate) {
|
|
49279
|
-
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${
|
|
49429
|
+
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${effectiveSlug}" already exists in this scope`);
|
|
49280
49430
|
}
|
|
49281
|
-
patch.slug = slug;
|
|
49282
49431
|
}
|
|
49432
|
+
if (projectId !== list.project_id)
|
|
49433
|
+
patch.project_id = projectId;
|
|
49283
49434
|
return store.upsert("task_lists", {
|
|
49284
49435
|
...list,
|
|
49285
49436
|
...patch,
|
|
@@ -49372,6 +49523,22 @@ async function addComment2(input, store, context) {
|
|
|
49372
49523
|
function redactComment2(comment) {
|
|
49373
49524
|
return { ...comment, content: redactEvidenceText(comment.content) };
|
|
49374
49525
|
}
|
|
49526
|
+
async function addPlanComment2(input, store, context) {
|
|
49527
|
+
const comment = {
|
|
49528
|
+
id: randomUUID4(),
|
|
49529
|
+
plan_id: input.plan_id,
|
|
49530
|
+
agent_id: input.agent_id ?? context?.agentId ?? null,
|
|
49531
|
+
session_id: input.session_id ?? context?.sessionId ?? null,
|
|
49532
|
+
content: redactEvidenceText(input.content),
|
|
49533
|
+
type: input.type ?? "comment",
|
|
49534
|
+
progress_pct: input.progress_pct ?? null,
|
|
49535
|
+
created_at: new Date().toISOString()
|
|
49536
|
+
};
|
|
49537
|
+
return store.upsert("plan_comments", comment, context);
|
|
49538
|
+
}
|
|
49539
|
+
function redactPlanComment(comment) {
|
|
49540
|
+
return { ...comment, content: redactEvidenceText(comment.content) };
|
|
49541
|
+
}
|
|
49375
49542
|
async function exportSnapshot(store) {
|
|
49376
49543
|
return {
|
|
49377
49544
|
exportedAt: new Date().toISOString(),
|
|
@@ -54191,6 +54358,924 @@ var init_task_manifest = __esm(() => {
|
|
|
54191
54358
|
init_types5();
|
|
54192
54359
|
});
|
|
54193
54360
|
|
|
54361
|
+
// src/task-subtree-transfer/types.ts
|
|
54362
|
+
var TODOS_TASK_SUBTREE_TRANSFER_ROUTE = "todos.task-subtree-transfer.v1", TODOS_TASK_SUBTREE_TRANSFER_SCHEMA_VERSION = 1, TodosTaskSubtreeTransferError;
|
|
54363
|
+
var init_types6 = __esm(() => {
|
|
54364
|
+
TodosTaskSubtreeTransferError = class TodosTaskSubtreeTransferError extends Error {
|
|
54365
|
+
code;
|
|
54366
|
+
details;
|
|
54367
|
+
constructor(code, message, details = {}) {
|
|
54368
|
+
super(message);
|
|
54369
|
+
this.code = code;
|
|
54370
|
+
this.details = details;
|
|
54371
|
+
this.name = "TodosTaskSubtreeTransferError";
|
|
54372
|
+
}
|
|
54373
|
+
};
|
|
54374
|
+
});
|
|
54375
|
+
|
|
54376
|
+
// src/task-subtree-transfer/canonical.ts
|
|
54377
|
+
function taskSubtreeTransferRequestDigest(input) {
|
|
54378
|
+
const { idempotency_key: _idempotencyKey, ...request } = input;
|
|
54379
|
+
return canonicalDigest(request);
|
|
54380
|
+
}
|
|
54381
|
+
function taskSubtreeTransferRollbackRequestDigest(input) {
|
|
54382
|
+
const { idempotency_key: _idempotencyKey, ...request } = input;
|
|
54383
|
+
return canonicalDigest(request);
|
|
54384
|
+
}
|
|
54385
|
+
function deriveTodosTaskSubtreeTransferIdempotencyKey(input) {
|
|
54386
|
+
return `tstk_${canonicalDigest({
|
|
54387
|
+
route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
|
|
54388
|
+
...input
|
|
54389
|
+
}).slice(0, 48)}`;
|
|
54390
|
+
}
|
|
54391
|
+
var init_canonical2 = __esm(() => {
|
|
54392
|
+
init_canonical();
|
|
54393
|
+
init_types6();
|
|
54394
|
+
});
|
|
54395
|
+
|
|
54396
|
+
// src/task-subtree-transfer/planner.ts
|
|
54397
|
+
function sortedTaskImage(task3) {
|
|
54398
|
+
return {
|
|
54399
|
+
task_id: task3.task_id,
|
|
54400
|
+
project_id: task3.project_id,
|
|
54401
|
+
parent_id: task3.parent_id,
|
|
54402
|
+
plan_id: task3.plan_id,
|
|
54403
|
+
task_list_id: task3.task_list_id,
|
|
54404
|
+
version: task3.version,
|
|
54405
|
+
updated_at: task3.updated_at
|
|
54406
|
+
};
|
|
54407
|
+
}
|
|
54408
|
+
function sourcePopulationDigest(tasks) {
|
|
54409
|
+
return canonicalDigest(tasks.map((task3) => ({
|
|
54410
|
+
...sortedTaskImage(task3),
|
|
54411
|
+
archived_at: task3.archived_at ?? null
|
|
54412
|
+
})).sort((left, right) => left.task_id.localeCompare(right.task_id)));
|
|
54413
|
+
}
|
|
54414
|
+
function closure(tasks, rootTaskId) {
|
|
54415
|
+
const byId = new Map(tasks.map((task3) => [task3.task_id, task3]));
|
|
54416
|
+
if (!byId.has(rootTaskId)) {
|
|
54417
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_NOT_FOUND", `Root task not found in the exact source project: ${rootTaskId}`, { root_task_id: rootTaskId });
|
|
54418
|
+
}
|
|
54419
|
+
const children = new Map;
|
|
54420
|
+
for (const task3 of tasks) {
|
|
54421
|
+
if (!task3.parent_id)
|
|
54422
|
+
continue;
|
|
54423
|
+
const siblings = children.get(task3.parent_id) ?? [];
|
|
54424
|
+
siblings.push(task3.task_id);
|
|
54425
|
+
children.set(task3.parent_id, siblings);
|
|
54426
|
+
}
|
|
54427
|
+
for (const ids2 of children.values())
|
|
54428
|
+
ids2.sort();
|
|
54429
|
+
const visiting = new Set;
|
|
54430
|
+
const visited = new Set;
|
|
54431
|
+
const result = [];
|
|
54432
|
+
const visit = (id) => {
|
|
54433
|
+
if (visiting.has(id)) {
|
|
54434
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_HIERARCHY_CYCLE", "Task hierarchy contains a cycle in the requested descendant closure", { task_id: id });
|
|
54435
|
+
}
|
|
54436
|
+
if (visited.has(id))
|
|
54437
|
+
return;
|
|
54438
|
+
const task3 = byId.get(id);
|
|
54439
|
+
if (!task3) {
|
|
54440
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_FOREIGN_REFERENCE", "Task hierarchy references a parent outside the exact source population", { task_id: id });
|
|
54441
|
+
}
|
|
54442
|
+
visiting.add(id);
|
|
54443
|
+
result.push(task3);
|
|
54444
|
+
for (const child of children.get(id) ?? [])
|
|
54445
|
+
visit(child);
|
|
54446
|
+
visiting.delete(id);
|
|
54447
|
+
visited.add(id);
|
|
54448
|
+
};
|
|
54449
|
+
visit(rootTaskId);
|
|
54450
|
+
return result.sort((left, right) => left.task_id.localeCompare(right.task_id));
|
|
54451
|
+
}
|
|
54452
|
+
function prepareTransfer(snapshot, input) {
|
|
54453
|
+
if (!snapshot.destination_project_found || !snapshot.destination_task_list_found || !snapshot.destination_parent_found) {
|
|
54454
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_FOREIGN_REFERENCE", "Destination project, task list, or parent does not match the exact requested target");
|
|
54455
|
+
}
|
|
54456
|
+
const moved = closure(snapshot.source_tasks, input.root_task_id);
|
|
54457
|
+
const movedIds = new Set(moved.map((task3) => task3.task_id));
|
|
54458
|
+
if (input.destination_parent_id && movedIds.has(input.destination_parent_id)) {
|
|
54459
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_HIERARCHY_CYCLE", "Destination parent cannot be inside the transferred subtree", { destination_parent_id: input.destination_parent_id });
|
|
54460
|
+
}
|
|
54461
|
+
const planIds = [...new Set(moved.map((task3) => task3.plan_id).filter((id) => Boolean(id)))].sort();
|
|
54462
|
+
const allPlanTasks = new Map;
|
|
54463
|
+
for (const task3 of snapshot.plan_tasks) {
|
|
54464
|
+
if (!task3.plan_id)
|
|
54465
|
+
continue;
|
|
54466
|
+
const rows = allPlanTasks.get(task3.plan_id) ?? [];
|
|
54467
|
+
rows.push(task3);
|
|
54468
|
+
allPlanTasks.set(task3.plan_id, rows);
|
|
54469
|
+
}
|
|
54470
|
+
const contained = [];
|
|
54471
|
+
const shared = [];
|
|
54472
|
+
const planById = new Map(snapshot.plans.map((plan) => [plan.plan_id, plan]));
|
|
54473
|
+
for (const planId of planIds) {
|
|
54474
|
+
const plan = planById.get(planId);
|
|
54475
|
+
if (!plan || plan.project_id !== input.source_project_id) {
|
|
54476
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_PLAN_CONFLICT", "A moved task references a plan absent from the exact source project", { plan_id: planId });
|
|
54477
|
+
}
|
|
54478
|
+
const members = allPlanTasks.get(planId) ?? [];
|
|
54479
|
+
if (members.length === 0) {
|
|
54480
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_PLAN_CONFLICT", "A moved task references a plan with no authoritative task membership", { plan_id: planId });
|
|
54481
|
+
}
|
|
54482
|
+
if (members.every((task3) => movedIds.has(task3.task_id)))
|
|
54483
|
+
contained.push(planId);
|
|
54484
|
+
else
|
|
54485
|
+
shared.push(planId);
|
|
54486
|
+
}
|
|
54487
|
+
const inspection = {
|
|
54488
|
+
source_project_id: input.source_project_id,
|
|
54489
|
+
destination_project_id: input.destination_project_id,
|
|
54490
|
+
destination_task_list_id: input.destination_task_list_id,
|
|
54491
|
+
root_task_id: input.root_task_id,
|
|
54492
|
+
destination_parent_id: input.destination_parent_id,
|
|
54493
|
+
expected_root_parent_id: moved.find((task3) => task3.task_id === input.root_task_id).parent_id,
|
|
54494
|
+
source_population_digest: sourcePopulationDigest(snapshot.source_tasks),
|
|
54495
|
+
expected_tasks: moved.map((task3) => ({ task_id: task3.task_id, version: task3.version })),
|
|
54496
|
+
contained_plan_ids: contained,
|
|
54497
|
+
shared_plan_ids: shared,
|
|
54498
|
+
complete: true
|
|
54499
|
+
};
|
|
54500
|
+
return {
|
|
54501
|
+
inspection,
|
|
54502
|
+
prior_tasks: moved.map(sortedTaskImage),
|
|
54503
|
+
prior_plans: contained.map((id) => planById.get(id)),
|
|
54504
|
+
task_plan_targets: new Map(moved.map((task3) => [task3.task_id, task3.plan_id]))
|
|
54505
|
+
};
|
|
54506
|
+
}
|
|
54507
|
+
function validateApplySnapshot(prepared, request, plans) {
|
|
54508
|
+
if (request.expected_root_parent_id !== prepared.inspection.expected_root_parent_id) {
|
|
54509
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "Root parent changed since inspection");
|
|
54510
|
+
}
|
|
54511
|
+
if (request.source_population_digest !== prepared.inspection.source_population_digest) {
|
|
54512
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_POPULATION_DRIFT", "Complete source-project task population changed since inspection");
|
|
54513
|
+
}
|
|
54514
|
+
const expected = request.expected_tasks.map((task3) => ({ task_id: task3.task_id, version: task3.version })).sort((left, right) => left.task_id.localeCompare(right.task_id));
|
|
54515
|
+
if (JSON.stringify(expected) !== JSON.stringify(prepared.inspection.expected_tasks)) {
|
|
54516
|
+
const actualById = new Map(prepared.inspection.expected_tasks.map((task3) => [task3.task_id, task3.version]));
|
|
54517
|
+
const sameIds = expected.length === prepared.inspection.expected_tasks.length && expected.every((task3) => actualById.has(task3.task_id));
|
|
54518
|
+
throw new TodosTaskSubtreeTransferError(sameIds ? "TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT" : "TODOS_TASK_SUBTREE_TRANSFER_CLOSURE_DRIFT", sameIds ? "At least one exact task version changed since inspection" : "Exact descendant closure changed since inspection");
|
|
54519
|
+
}
|
|
54520
|
+
const requestedSources = request.shared_plan_splits.map((mapping) => mapping.source_plan_id).sort();
|
|
54521
|
+
if (JSON.stringify(requestedSources) !== JSON.stringify(prepared.inspection.shared_plan_ids)) {
|
|
54522
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_PARTIAL_PLAN", "Shared plans require one exact explicit destination-plan mapping each", {
|
|
54523
|
+
expected_shared_plan_ids: prepared.inspection.shared_plan_ids,
|
|
54524
|
+
received_shared_plan_ids: requestedSources
|
|
54525
|
+
});
|
|
54526
|
+
}
|
|
54527
|
+
const planById = new Map(plans.map((plan) => [plan.plan_id, plan]));
|
|
54528
|
+
const destinationIds = new Set;
|
|
54529
|
+
for (const mapping of request.shared_plan_splits) {
|
|
54530
|
+
if (destinationIds.has(mapping.destination_plan_id)) {
|
|
54531
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_PLAN_CONFLICT", "Two shared source plans cannot collapse into one destination plan");
|
|
54532
|
+
}
|
|
54533
|
+
destinationIds.add(mapping.destination_plan_id);
|
|
54534
|
+
const destination = planById.get(mapping.destination_plan_id);
|
|
54535
|
+
if (!destination || destination.project_id !== request.destination_project_id || destination.task_list_id !== request.destination_task_list_id) {
|
|
54536
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_PLAN_CONFLICT", "Shared-plan destination does not match the exact destination project and task list", { destination_plan_id: mapping.destination_plan_id });
|
|
54537
|
+
}
|
|
54538
|
+
for (const [taskId, planId] of prepared.task_plan_targets) {
|
|
54539
|
+
if (planId === mapping.source_plan_id) {
|
|
54540
|
+
prepared.task_plan_targets.set(taskId, mapping.destination_plan_id);
|
|
54541
|
+
}
|
|
54542
|
+
}
|
|
54543
|
+
}
|
|
54544
|
+
}
|
|
54545
|
+
var init_planner = __esm(() => {
|
|
54546
|
+
init_canonical2();
|
|
54547
|
+
init_types6();
|
|
54548
|
+
});
|
|
54549
|
+
|
|
54550
|
+
// src/task-subtree-transfer/schema-sql.ts
|
|
54551
|
+
function postgresTodosTaskSubtreeTransferSchemaSql() {
|
|
54552
|
+
return [
|
|
54553
|
+
`CREATE TABLE IF NOT EXISTS todos_task_subtree_transfer_receipts (
|
|
54554
|
+
receipt_id text PRIMARY KEY,
|
|
54555
|
+
tenant_id text NOT NULL,
|
|
54556
|
+
kind text NOT NULL CHECK(kind IN ('apply', 'rollback')),
|
|
54557
|
+
operation_id text NOT NULL,
|
|
54558
|
+
step_id text NOT NULL,
|
|
54559
|
+
idempotency_key text NOT NULL,
|
|
54560
|
+
request_digest text NOT NULL,
|
|
54561
|
+
precondition_digest text NOT NULL,
|
|
54562
|
+
result_digest text NOT NULL,
|
|
54563
|
+
apply_receipt_id text,
|
|
54564
|
+
request_json jsonb NOT NULL,
|
|
54565
|
+
result_json jsonb NOT NULL,
|
|
54566
|
+
created_at timestamptz NOT NULL,
|
|
54567
|
+
UNIQUE(tenant_id, kind, operation_id, step_id),
|
|
54568
|
+
UNIQUE(tenant_id, kind, idempotency_key)
|
|
54569
|
+
)`,
|
|
54570
|
+
`CREATE OR REPLACE FUNCTION reject_todos_task_subtree_transfer_receipt_mutation()
|
|
54571
|
+
RETURNS trigger LANGUAGE plpgsql AS $$
|
|
54572
|
+
BEGIN
|
|
54573
|
+
RAISE EXCEPTION 'todos task subtree transfer receipts are immutable';
|
|
54574
|
+
END $$`,
|
|
54575
|
+
`DROP TRIGGER IF EXISTS todos_task_subtree_transfer_receipts_immutable_update
|
|
54576
|
+
ON todos_task_subtree_transfer_receipts`,
|
|
54577
|
+
`CREATE TRIGGER todos_task_subtree_transfer_receipts_immutable_update
|
|
54578
|
+
BEFORE UPDATE OR DELETE ON todos_task_subtree_transfer_receipts
|
|
54579
|
+
FOR EACH ROW EXECUTE FUNCTION reject_todos_task_subtree_transfer_receipt_mutation()`
|
|
54580
|
+
];
|
|
54581
|
+
}
|
|
54582
|
+
|
|
54583
|
+
// src/task-subtree-transfer/postgres.ts
|
|
54584
|
+
function safeIdentifier3(value, fallback) {
|
|
54585
|
+
const candidate = value ?? fallback;
|
|
54586
|
+
if (!/^[a-z_][a-z0-9_]*$/.test(candidate)) {
|
|
54587
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "PostgreSQL table name must be a safe identifier");
|
|
54588
|
+
}
|
|
54589
|
+
return candidate;
|
|
54590
|
+
}
|
|
54591
|
+
function timestamp5(value) {
|
|
54592
|
+
return value instanceof Date ? value.toISOString() : new Date(String(value)).toISOString();
|
|
54593
|
+
}
|
|
54594
|
+
function payloadOf(row) {
|
|
54595
|
+
return typeof row.payload === "string" ? JSON.parse(row.payload) : row.payload;
|
|
54596
|
+
}
|
|
54597
|
+
function taskRecord(row) {
|
|
54598
|
+
const payload = payloadOf(row);
|
|
54599
|
+
return {
|
|
54600
|
+
task_id: String(payload.id ?? row.object_id),
|
|
54601
|
+
project_id: payload.project_id == null ? null : String(payload.project_id),
|
|
54602
|
+
parent_id: payload.parent_id == null ? null : String(payload.parent_id),
|
|
54603
|
+
plan_id: payload.plan_id == null ? null : String(payload.plan_id),
|
|
54604
|
+
task_list_id: payload.task_list_id == null ? null : String(payload.task_list_id),
|
|
54605
|
+
version: Number(payload.version ?? row.version ?? 1),
|
|
54606
|
+
updated_at: timestamp5(payload.updated_at ?? row.updated_at),
|
|
54607
|
+
archived_at: payload.archived_at == null ? null : String(payload.archived_at)
|
|
54608
|
+
};
|
|
54609
|
+
}
|
|
54610
|
+
function planRecord(row) {
|
|
54611
|
+
const payload = payloadOf(row);
|
|
54612
|
+
return {
|
|
54613
|
+
plan_id: String(payload.id ?? row.object_id),
|
|
54614
|
+
project_id: payload.project_id == null ? null : String(payload.project_id),
|
|
54615
|
+
task_list_id: payload.task_list_id == null ? null : String(payload.task_list_id),
|
|
54616
|
+
updated_at: timestamp5(payload.updated_at ?? row.updated_at)
|
|
54617
|
+
};
|
|
54618
|
+
}
|
|
54619
|
+
|
|
54620
|
+
class PostgresTodosTaskSubtreeTransferBackend {
|
|
54621
|
+
client;
|
|
54622
|
+
kind = "postgresql";
|
|
54623
|
+
service;
|
|
54624
|
+
tableName;
|
|
54625
|
+
tenantId;
|
|
54626
|
+
schemaReady = null;
|
|
54627
|
+
constructor(client, options = {}) {
|
|
54628
|
+
this.client = client;
|
|
54629
|
+
this.service = options.service ?? "todos";
|
|
54630
|
+
this.tableName = safeIdentifier3(options.tableName, DEFAULT_TODOS_POSTGRES_SYNC_TABLE);
|
|
54631
|
+
this.tenantId = options.tenantId ?? "default";
|
|
54632
|
+
}
|
|
54633
|
+
async ensureSchema() {
|
|
54634
|
+
this.schemaReady ??= (async () => {
|
|
54635
|
+
for (const sql of postgresTodosSyncSchemaSql(this.tableName))
|
|
54636
|
+
await this.client.query(sql);
|
|
54637
|
+
for (const sql of postgresTodosTaskSubtreeTransferSchemaSql())
|
|
54638
|
+
await this.client.query(sql);
|
|
54639
|
+
})();
|
|
54640
|
+
await this.schemaReady;
|
|
54641
|
+
}
|
|
54642
|
+
async snapshot(client, input, forUpdate = false) {
|
|
54643
|
+
const lock = forUpdate ? " FOR UPDATE" : "";
|
|
54644
|
+
const source3 = await client.query(`SELECT object_id, payload, updated_at, version FROM ${this.tableName}
|
|
54645
|
+
WHERE service = $1 AND object_type = 'tasks' AND deleted_at IS NULL
|
|
54646
|
+
AND payload->>'project_id' = $2 ORDER BY object_id${lock}`, [this.service, input.source_project_id]);
|
|
54647
|
+
const allTasks = await client.query(`SELECT object_id, payload, updated_at, version FROM ${this.tableName}
|
|
54648
|
+
WHERE service = $1 AND object_type = 'tasks' AND deleted_at IS NULL ORDER BY object_id${lock}`, [this.service]);
|
|
54649
|
+
const plans = await client.query(`SELECT object_id, payload, updated_at, version FROM ${this.tableName}
|
|
54650
|
+
WHERE service = $1 AND object_type = 'plans' AND deleted_at IS NULL ORDER BY object_id${lock}`, [this.service]);
|
|
54651
|
+
const destinationProject = await client.query(`SELECT 1 FROM ${this.tableName}
|
|
54652
|
+
WHERE service = $1 AND object_type = 'projects' AND object_id = $2
|
|
54653
|
+
AND deleted_at IS NULL LIMIT 1${lock}`, [this.service, input.destination_project_id]);
|
|
54654
|
+
const destinationList = await client.query(`SELECT 1 FROM ${this.tableName}
|
|
54655
|
+
WHERE service = $1 AND object_type = 'task_lists' AND object_id = $2
|
|
54656
|
+
AND deleted_at IS NULL AND payload->>'project_id' = $3 LIMIT 1`, [this.service, input.destination_task_list_id, input.destination_project_id]);
|
|
54657
|
+
const destinationParent = input.destination_parent_id === null ? true : (await client.query(`SELECT 1 FROM ${this.tableName}
|
|
54658
|
+
WHERE service = $1 AND object_type = 'tasks' AND object_id = $2
|
|
54659
|
+
AND deleted_at IS NULL AND payload->>'project_id' = $3 LIMIT 1`, [this.service, input.destination_parent_id, input.destination_project_id])).rows.length === 1;
|
|
54660
|
+
return {
|
|
54661
|
+
source_tasks: source3.rows.map(taskRecord),
|
|
54662
|
+
plan_tasks: allTasks.rows.map(taskRecord),
|
|
54663
|
+
plans: plans.rows.map(planRecord),
|
|
54664
|
+
destination_project_found: destinationProject.rows.length === 1,
|
|
54665
|
+
destination_task_list_found: destinationList.rows.length === 1,
|
|
54666
|
+
destination_parent_found: destinationParent
|
|
54667
|
+
};
|
|
54668
|
+
}
|
|
54669
|
+
async inspect(input) {
|
|
54670
|
+
await this.ensureSchema();
|
|
54671
|
+
return prepareTransfer(await this.snapshot(this.client, input), input).inspection;
|
|
54672
|
+
}
|
|
54673
|
+
async insertPayload(client, task3, update, now4) {
|
|
54674
|
+
const current = await client.query(`SELECT payload FROM ${this.tableName}
|
|
54675
|
+
WHERE service = $1 AND object_type = 'tasks' AND object_id = $2
|
|
54676
|
+
AND deleted_at IS NULL FOR UPDATE`, [this.service, task3.task_id]);
|
|
54677
|
+
const row = current.rows[0];
|
|
54678
|
+
if (!row)
|
|
54679
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_NOT_FOUND", `Task not found: ${task3.task_id}`);
|
|
54680
|
+
const payload = payloadOf(row);
|
|
54681
|
+
if (Number(payload.version ?? 1) !== task3.version) {
|
|
54682
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "Task changed during transfer", { task_id: task3.task_id });
|
|
54683
|
+
}
|
|
54684
|
+
const next = {
|
|
54685
|
+
...payload,
|
|
54686
|
+
...update,
|
|
54687
|
+
version: task3.version + 1,
|
|
54688
|
+
updated_at: now4
|
|
54689
|
+
};
|
|
54690
|
+
const result = await client.query(`UPDATE ${this.tableName} SET payload = $1::jsonb, updated_at = $2, version = $3
|
|
54691
|
+
WHERE service = $4 AND object_type = 'tasks' AND object_id = $5
|
|
54692
|
+
AND deleted_at IS NULL AND version = $6
|
|
54693
|
+
RETURNING object_id`, [canonicalJson(next), now4, task3.version + 1, this.service, task3.task_id, task3.version]);
|
|
54694
|
+
if (result.rows.length !== 1) {
|
|
54695
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "Task CAS failed", { task_id: task3.task_id });
|
|
54696
|
+
}
|
|
54697
|
+
}
|
|
54698
|
+
async apply(input, options) {
|
|
54699
|
+
await this.ensureSchema();
|
|
54700
|
+
if (!this.client.transaction) {
|
|
54701
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ATOMICITY_UNAVAILABLE", "PostgreSQL subtree transfer requires an authoritative transaction-capable client");
|
|
54702
|
+
}
|
|
54703
|
+
return this.client.transaction(async (tx) => {
|
|
54704
|
+
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended('todos:task-parent-integrity', 0))");
|
|
54705
|
+
const requestDigest = taskSubtreeTransferRequestDigest(input);
|
|
54706
|
+
const existing = await tx.query(`SELECT result_json, request_digest, precondition_digest, idempotency_key
|
|
54707
|
+
FROM todos_task_subtree_transfer_receipts
|
|
54708
|
+
WHERE tenant_id = $1 AND kind = 'apply'
|
|
54709
|
+
AND (idempotency_key = $2 OR (operation_id = $3 AND step_id = $4))
|
|
54710
|
+
ORDER BY created_at ASC LIMIT 1`, [this.tenantId, input.idempotency_key, input.operation_id, input.step_id]);
|
|
54711
|
+
if (existing.rows[0]) {
|
|
54712
|
+
const row = existing.rows[0];
|
|
54713
|
+
if (row.request_digest !== requestDigest || row.precondition_digest !== input.precondition_digest || row.idempotency_key !== input.idempotency_key) {
|
|
54714
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_IDEMPOTENCY_CONFLICT", "The operation was already accepted for a different request");
|
|
54715
|
+
}
|
|
54716
|
+
return { ...typeof row.result_json === "string" ? JSON.parse(row.result_json) : row.result_json, duplicate: true };
|
|
54717
|
+
}
|
|
54718
|
+
const snapshot = await this.snapshot(tx, input, true);
|
|
54719
|
+
const prepared = prepareTransfer(snapshot, input);
|
|
54720
|
+
validateApplySnapshot(prepared, input, snapshot.plans);
|
|
54721
|
+
const now4 = options.now?.() ?? new Date().toISOString();
|
|
54722
|
+
const priorImage = {
|
|
54723
|
+
tasks: prepared.prior_tasks,
|
|
54724
|
+
plans: prepared.prior_plans
|
|
54725
|
+
};
|
|
54726
|
+
for (const task3 of prepared.prior_tasks) {
|
|
54727
|
+
await this.insertPayload(tx, task3, {
|
|
54728
|
+
project_id: input.destination_project_id,
|
|
54729
|
+
task_list_id: input.destination_task_list_id,
|
|
54730
|
+
plan_id: prepared.task_plan_targets.get(task3.task_id) ?? null,
|
|
54731
|
+
parent_id: task3.task_id === input.root_task_id ? input.destination_parent_id : task3.parent_id
|
|
54732
|
+
}, now4);
|
|
54733
|
+
}
|
|
54734
|
+
if (options.faultInjector && await options.faultInjector("after_task_writes"))
|
|
54735
|
+
throw new Error("Injected task-subtree-transfer fault at after_task_writes");
|
|
54736
|
+
for (const plan of prepared.prior_plans) {
|
|
54737
|
+
const row = await tx.query(`SELECT payload FROM ${this.tableName}
|
|
54738
|
+
WHERE service = $1 AND object_type = 'plans' AND object_id = $2
|
|
54739
|
+
AND deleted_at IS NULL FOR UPDATE`, [this.service, plan.plan_id]);
|
|
54740
|
+
const current = row.rows[0] ? payloadOf(row.rows[0]) : null;
|
|
54741
|
+
if (!current || String(current.project_id ?? "") !== String(plan.project_id ?? "") || (current.task_list_id ?? null) !== plan.task_list_id || timestamp5(current.updated_at ?? row.rows[0]?.updated_at) !== plan.updated_at) {
|
|
54742
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "Contained plan changed during transfer", { plan_id: plan.plan_id });
|
|
54743
|
+
}
|
|
54744
|
+
const result2 = await tx.query(`UPDATE ${this.tableName} SET payload = jsonb_set(jsonb_set(jsonb_set(payload, '{project_id}', to_jsonb($1::text)), '{task_list_id}', to_jsonb($2::text)), '{updated_at}', to_jsonb($3::text)), updated_at = $3
|
|
54745
|
+
WHERE service = $4 AND object_type = 'plans' AND object_id = $5 AND deleted_at IS NULL
|
|
54746
|
+
RETURNING object_id`, [input.destination_project_id, input.destination_task_list_id, now4, this.service, plan.plan_id]);
|
|
54747
|
+
if (result2.rows.length !== 1) {
|
|
54748
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_CAS_CONFLICT", "Contained plan update did not affect exactly one row", { plan_id: plan.plan_id });
|
|
54749
|
+
}
|
|
54750
|
+
}
|
|
54751
|
+
if (options.faultInjector && await options.faultInjector("after_plan_writes"))
|
|
54752
|
+
throw new Error("Injected task-subtree-transfer fault at after_plan_writes");
|
|
54753
|
+
const postImage = {
|
|
54754
|
+
tasks: prepared.prior_tasks.map((task3) => ({
|
|
54755
|
+
...task3,
|
|
54756
|
+
project_id: input.destination_project_id,
|
|
54757
|
+
task_list_id: input.destination_task_list_id,
|
|
54758
|
+
plan_id: prepared.task_plan_targets.get(task3.task_id) ?? null,
|
|
54759
|
+
parent_id: task3.task_id === input.root_task_id ? input.destination_parent_id : task3.parent_id,
|
|
54760
|
+
version: task3.version + 1,
|
|
54761
|
+
updated_at: now4
|
|
54762
|
+
})),
|
|
54763
|
+
plans: prepared.prior_plans.map((plan) => ({ ...plan, project_id: input.destination_project_id, task_list_id: input.destination_task_list_id, updated_at: now4 }))
|
|
54764
|
+
};
|
|
54765
|
+
const resultDigest = canonicalDigest({ route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE, request_digest: requestDigest, prior_image: priorImage, post_image: postImage });
|
|
54766
|
+
const receipt = {
|
|
54767
|
+
receipt_id: deterministicUuid(TODOS_TASK_SUBTREE_TRANSFER_ROUTE, "apply", input.operation_id, input.step_id, input.idempotency_key, requestDigest),
|
|
54768
|
+
authority: "todos",
|
|
54769
|
+
route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
|
|
54770
|
+
schema_version: 1,
|
|
54771
|
+
kind: "apply",
|
|
54772
|
+
operation_id: input.operation_id,
|
|
54773
|
+
step_id: input.step_id,
|
|
54774
|
+
idempotency_key: input.idempotency_key,
|
|
54775
|
+
request_digest: requestDigest,
|
|
54776
|
+
precondition_digest: input.precondition_digest,
|
|
54777
|
+
result_digest: resultDigest,
|
|
54778
|
+
apply_receipt_id: null,
|
|
54779
|
+
source_project_id: input.source_project_id,
|
|
54780
|
+
destination_project_id: input.destination_project_id,
|
|
54781
|
+
destination_task_list_id: input.destination_task_list_id,
|
|
54782
|
+
root_task_id: input.root_task_id,
|
|
54783
|
+
source_population_digest: input.source_population_digest,
|
|
54784
|
+
prior_image: priorImage,
|
|
54785
|
+
post_image: postImage,
|
|
54786
|
+
shared_plan_splits: input.shared_plan_splits,
|
|
54787
|
+
created_at: now4
|
|
54788
|
+
};
|
|
54789
|
+
const result = { duplicate: false, receipt, moved_task_ids: prepared.prior_tasks.map((task3) => task3.task_id), moved_plan_ids: prepared.prior_plans.map((plan) => plan.plan_id), complete: true };
|
|
54790
|
+
await tx.query(`INSERT INTO todos_task_subtree_transfer_receipts (
|
|
54791
|
+
receipt_id, tenant_id, kind, operation_id, step_id, idempotency_key,
|
|
54792
|
+
request_digest, precondition_digest, result_digest, apply_receipt_id,
|
|
54793
|
+
request_json, result_json, created_at
|
|
54794
|
+
) VALUES ($1, $2, 'apply', $3, $4, $5, $6, $7, $8, NULL, $9::jsonb, $10::jsonb, $11)`, [receipt.receipt_id, this.tenantId, receipt.operation_id, receipt.step_id, receipt.idempotency_key, receipt.request_digest, receipt.precondition_digest, receipt.result_digest, canonicalJson(input), canonicalJson(result), now4]);
|
|
54795
|
+
if (options.faultInjector && await options.faultInjector("after_receipt_write"))
|
|
54796
|
+
throw new Error("Injected task-subtree-transfer fault at after_receipt_write");
|
|
54797
|
+
return result;
|
|
54798
|
+
});
|
|
54799
|
+
}
|
|
54800
|
+
async readExact(receiptId2) {
|
|
54801
|
+
await this.ensureSchema();
|
|
54802
|
+
const row = await this.client.query(`SELECT result_json FROM todos_task_subtree_transfer_receipts WHERE tenant_id = $1 AND receipt_id = $2 LIMIT 1`, [this.tenantId, receiptId2]);
|
|
54803
|
+
if (!row.rows[0])
|
|
54804
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_RECEIPT_NOT_FOUND", `Transfer receipt not found: ${receiptId2}`);
|
|
54805
|
+
return { ...typeof row.rows[0].result_json === "string" ? JSON.parse(String(row.rows[0].result_json)) : row.rows[0].result_json, duplicate: false };
|
|
54806
|
+
}
|
|
54807
|
+
async rollback(input, options) {
|
|
54808
|
+
await this.ensureSchema();
|
|
54809
|
+
if (!this.client.transaction)
|
|
54810
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ATOMICITY_UNAVAILABLE", "PostgreSQL subtree rollback requires a transaction-capable client");
|
|
54811
|
+
return this.client.transaction(async (tx) => {
|
|
54812
|
+
await tx.query("SELECT pg_advisory_xact_lock(hashtextextended('todos:task-parent-integrity', 0))");
|
|
54813
|
+
const requestDigest = taskSubtreeTransferRollbackRequestDigest(input);
|
|
54814
|
+
const existing = await tx.query(`SELECT result_json, request_digest, precondition_digest, idempotency_key
|
|
54815
|
+
FROM todos_task_subtree_transfer_receipts
|
|
54816
|
+
WHERE tenant_id = $1 AND kind = 'rollback'
|
|
54817
|
+
AND (idempotency_key = $2 OR (operation_id = $3 AND step_id = $4))
|
|
54818
|
+
ORDER BY created_at ASC LIMIT 1`, [this.tenantId, input.idempotency_key, input.operation_id, input.step_id]);
|
|
54819
|
+
if (existing.rows[0]) {
|
|
54820
|
+
const row = existing.rows[0];
|
|
54821
|
+
if (row.request_digest !== requestDigest || row.precondition_digest !== input.precondition_digest || row.idempotency_key !== input.idempotency_key)
|
|
54822
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_IDEMPOTENCY_CONFLICT", "The rollback operation was already accepted for a different request");
|
|
54823
|
+
return { ...typeof row.result_json === "string" ? JSON.parse(String(row.result_json)) : row.result_json, duplicate: true };
|
|
54824
|
+
}
|
|
54825
|
+
const applyRow = await tx.query(`SELECT result_json FROM todos_task_subtree_transfer_receipts WHERE tenant_id = $1 AND kind = 'apply' AND receipt_id = $2 LIMIT 1`, [this.tenantId, input.receipt_id]);
|
|
54826
|
+
if (!applyRow.rows[0])
|
|
54827
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_RECEIPT_NOT_FOUND", `Apply receipt not found: ${input.receipt_id}`);
|
|
54828
|
+
const applied = typeof applyRow.rows[0].result_json === "string" ? JSON.parse(String(applyRow.rows[0].result_json)) : applyRow.rows[0].result_json;
|
|
54829
|
+
if (applied.receipt.operation_id !== input.operation_id) {
|
|
54830
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Rollback operation does not match the apply receipt");
|
|
54831
|
+
}
|
|
54832
|
+
const expectedPrecondition = canonicalDigest({ route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE, direction: "rollback", operation_id: input.operation_id, step_id: input.step_id, apply_receipt_id: input.receipt_id, apply_result_digest: applied.receipt.result_digest });
|
|
54833
|
+
if (input.precondition_digest !== expectedPrecondition)
|
|
54834
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_DIGEST_MISMATCH", "Rollback precondition digest does not match the exact apply receipt", { expected_precondition_digest: expectedPrecondition });
|
|
54835
|
+
const now4 = options.now?.() ?? new Date().toISOString();
|
|
54836
|
+
for (const task3 of applied.receipt.post_image.tasks) {
|
|
54837
|
+
const row = await tx.query(`SELECT payload FROM ${this.tableName} WHERE service = $1 AND object_type = 'tasks' AND object_id = $2 AND deleted_at IS NULL FOR UPDATE`, [this.service, task3.task_id]);
|
|
54838
|
+
if (!row.rows[0])
|
|
54839
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Transferred task disappeared", { task_id: task3.task_id });
|
|
54840
|
+
const payload = payloadOf(row.rows[0]);
|
|
54841
|
+
if (Number(payload.version ?? 1) !== task3.version || String(payload.project_id ?? "") !== String(task3.project_id ?? "") || (payload.parent_id ?? null) !== task3.parent_id || (payload.plan_id ?? null) !== task3.plan_id || (payload.task_list_id ?? null) !== task3.task_list_id)
|
|
54842
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Transferred task changed after apply", { task_id: task3.task_id });
|
|
54843
|
+
}
|
|
54844
|
+
for (const task3 of applied.receipt.prior_image.tasks) {
|
|
54845
|
+
const row = await tx.query(`SELECT payload FROM ${this.tableName} WHERE service = $1 AND object_type = 'tasks' AND object_id = $2 AND deleted_at IS NULL`, [this.service, task3.task_id]);
|
|
54846
|
+
const payload = row.rows[0] ? payloadOf(row.rows[0]) : null;
|
|
54847
|
+
if (!payload)
|
|
54848
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Task disappeared during rollback", { task_id: task3.task_id });
|
|
54849
|
+
const next = { ...payload, project_id: task3.project_id, task_list_id: task3.task_list_id, plan_id: task3.plan_id, parent_id: task3.parent_id, version: task3.version + 2, updated_at: now4 };
|
|
54850
|
+
const result2 = await tx.query(`UPDATE ${this.tableName} SET payload = $1::jsonb, updated_at = $2, version = $3
|
|
54851
|
+
WHERE service = $4 AND object_type = 'tasks' AND object_id = $5 AND deleted_at IS NULL AND version = $6
|
|
54852
|
+
RETURNING object_id`, [canonicalJson(next), now4, task3.version + 2, this.service, task3.task_id, task3.version + 1]);
|
|
54853
|
+
if (result2.rows.length !== 1) {
|
|
54854
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Task rollback did not affect exactly one row", { task_id: task3.task_id });
|
|
54855
|
+
}
|
|
54856
|
+
}
|
|
54857
|
+
if (options.faultInjector && await options.faultInjector("after_rollback_task_writes"))
|
|
54858
|
+
throw new Error("Injected task-subtree-transfer fault at after_rollback_task_writes");
|
|
54859
|
+
for (const plan of applied.receipt.prior_image.plans) {
|
|
54860
|
+
const post = applied.receipt.post_image.plans.find((candidate) => candidate.plan_id === plan.plan_id);
|
|
54861
|
+
const result2 = await tx.query(`UPDATE ${this.tableName}
|
|
54862
|
+
SET payload = jsonb_set(
|
|
54863
|
+
jsonb_set(
|
|
54864
|
+
jsonb_set(payload, '{project_id}', to_jsonb($1::text)),
|
|
54865
|
+
'{task_list_id}',
|
|
54866
|
+
COALESCE(to_jsonb($2::text), 'null'::jsonb)
|
|
54867
|
+
),
|
|
54868
|
+
'{updated_at}',
|
|
54869
|
+
to_jsonb($3::text)
|
|
54870
|
+
), updated_at = $3
|
|
54871
|
+
WHERE service = $4 AND object_type = 'plans' AND object_id = $5 AND deleted_at IS NULL
|
|
54872
|
+
AND payload->>'project_id' = $6 AND COALESCE(payload->>'task_list_id', '') = COALESCE($7, '') AND payload->>'updated_at' = $8
|
|
54873
|
+
RETURNING object_id`, [plan.project_id, plan.task_list_id, now4, this.service, plan.plan_id, post.project_id, post.task_list_id, post.updated_at]);
|
|
54874
|
+
if (result2.rows.length !== 1) {
|
|
54875
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_ROLLBACK_CONFLICT", "Contained plan rollback did not affect exactly one row", { plan_id: plan.plan_id });
|
|
54876
|
+
}
|
|
54877
|
+
}
|
|
54878
|
+
if (options.faultInjector && await options.faultInjector("after_rollback_plan_writes"))
|
|
54879
|
+
throw new Error("Injected task-subtree-transfer fault at after_rollback_plan_writes");
|
|
54880
|
+
const restored = {
|
|
54881
|
+
tasks: applied.receipt.prior_image.tasks.map((task3) => ({ ...task3, version: task3.version + 2, updated_at: now4 })),
|
|
54882
|
+
plans: applied.receipt.prior_image.plans.map((plan) => ({ ...plan, updated_at: now4 }))
|
|
54883
|
+
};
|
|
54884
|
+
const resultDigest = canonicalDigest({ route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE, direction: "rollback", apply_receipt_id: input.receipt_id, prior_image: applied.receipt.post_image, post_image: restored });
|
|
54885
|
+
const receipt = { ...applied.receipt, receipt_id: deterministicUuid(TODOS_TASK_SUBTREE_TRANSFER_ROUTE, "rollback", input.operation_id, input.step_id, input.idempotency_key, input.receipt_id), kind: "rollback", operation_id: input.operation_id, step_id: input.step_id, idempotency_key: input.idempotency_key, request_digest: requestDigest, precondition_digest: input.precondition_digest, result_digest: resultDigest, apply_receipt_id: input.receipt_id, prior_image: applied.receipt.post_image, post_image: restored, created_at: now4 };
|
|
54886
|
+
const result = { duplicate: false, receipt, moved_task_ids: applied.moved_task_ids, moved_plan_ids: applied.moved_plan_ids, complete: true };
|
|
54887
|
+
await tx.query(`INSERT INTO todos_task_subtree_transfer_receipts (
|
|
54888
|
+
receipt_id, tenant_id, kind, operation_id, step_id, idempotency_key,
|
|
54889
|
+
request_digest, precondition_digest, result_digest, apply_receipt_id,
|
|
54890
|
+
request_json, result_json, created_at
|
|
54891
|
+
) VALUES ($1, $2, 'rollback', $3, $4, $5, $6, $7, $8, $9, $10::jsonb, $11::jsonb, $12)`, [receipt.receipt_id, this.tenantId, receipt.operation_id, receipt.step_id, receipt.idempotency_key, receipt.request_digest, receipt.precondition_digest, receipt.result_digest, receipt.apply_receipt_id, canonicalJson(input), canonicalJson(result), now4]);
|
|
54892
|
+
if (options.faultInjector && await options.faultInjector("after_rollback_receipt_write"))
|
|
54893
|
+
throw new Error("Injected task-subtree-transfer fault at after_rollback_receipt_write");
|
|
54894
|
+
return result;
|
|
54895
|
+
});
|
|
54896
|
+
}
|
|
54897
|
+
}
|
|
54898
|
+
var init_postgres4 = __esm(() => {
|
|
54899
|
+
init_postgres_sync();
|
|
54900
|
+
init_canonical2();
|
|
54901
|
+
init_planner();
|
|
54902
|
+
init_types6();
|
|
54903
|
+
});
|
|
54904
|
+
|
|
54905
|
+
// src/task-subtree-transfer/sqlite.ts
|
|
54906
|
+
var sqliteTails2;
|
|
54907
|
+
var init_sqlite3 = __esm(() => {
|
|
54908
|
+
init_canonical2();
|
|
54909
|
+
init_planner();
|
|
54910
|
+
init_types6();
|
|
54911
|
+
sqliteTails2 = new WeakMap;
|
|
54912
|
+
});
|
|
54913
|
+
|
|
54914
|
+
// src/task-subtree-transfer/authority.ts
|
|
54915
|
+
function boundedString(value, field, max = 200) {
|
|
54916
|
+
if (typeof value !== "string" || value.length === 0 || value.length > max) {
|
|
54917
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", `${field} must be a non-empty bounded string`);
|
|
54918
|
+
}
|
|
54919
|
+
return value;
|
|
54920
|
+
}
|
|
54921
|
+
function uuid3(value, field) {
|
|
54922
|
+
const parsed = boundedString(value, field, 80);
|
|
54923
|
+
if (!UUID.test(parsed)) {
|
|
54924
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", `${field} must be a UUID`);
|
|
54925
|
+
}
|
|
54926
|
+
return parsed;
|
|
54927
|
+
}
|
|
54928
|
+
function digest2(value, field) {
|
|
54929
|
+
const parsed = boundedString(value, field, 64);
|
|
54930
|
+
if (!DIGEST.test(parsed)) {
|
|
54931
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", `${field} must be a SHA-256 digest`);
|
|
54932
|
+
}
|
|
54933
|
+
return parsed.toLowerCase();
|
|
54934
|
+
}
|
|
54935
|
+
function nullableUuid(value, field) {
|
|
54936
|
+
if (value === null)
|
|
54937
|
+
return null;
|
|
54938
|
+
return uuid3(value, field);
|
|
54939
|
+
}
|
|
54940
|
+
function strictObject(input, fields) {
|
|
54941
|
+
if (input === null || typeof input !== "object" || Array.isArray(input)) {
|
|
54942
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "Request must be a JSON object");
|
|
54943
|
+
}
|
|
54944
|
+
const object = input;
|
|
54945
|
+
const allowed = new Set(fields);
|
|
54946
|
+
for (const key2 of Object.keys(object)) {
|
|
54947
|
+
if (!allowed.has(key2)) {
|
|
54948
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", `Unknown field: ${key2}`);
|
|
54949
|
+
}
|
|
54950
|
+
}
|
|
54951
|
+
return object;
|
|
54952
|
+
}
|
|
54953
|
+
function parseTodosTaskSubtreeTransferInspect(input) {
|
|
54954
|
+
const value = strictObject(input, INSPECT_FIELDS);
|
|
54955
|
+
const request = {
|
|
54956
|
+
source_project_id: uuid3(value.source_project_id, "source_project_id"),
|
|
54957
|
+
destination_project_id: uuid3(value.destination_project_id, "destination_project_id"),
|
|
54958
|
+
destination_task_list_id: uuid3(value.destination_task_list_id, "destination_task_list_id"),
|
|
54959
|
+
root_task_id: uuid3(value.root_task_id, "root_task_id"),
|
|
54960
|
+
destination_parent_id: nullableUuid(value.destination_parent_id, "destination_parent_id")
|
|
54961
|
+
};
|
|
54962
|
+
if (request.source_project_id === request.destination_project_id) {
|
|
54963
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "source_project_id and destination_project_id must identify different Projects");
|
|
54964
|
+
}
|
|
54965
|
+
return request;
|
|
54966
|
+
}
|
|
54967
|
+
function parseTodosTaskSubtreeTransferApply(input) {
|
|
54968
|
+
const value = strictObject(input, APPLY_FIELDS);
|
|
54969
|
+
if (value.version !== 1) {
|
|
54970
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "version must be 1");
|
|
54971
|
+
}
|
|
54972
|
+
if (!Array.isArray(value.expected_tasks) || value.expected_tasks.length === 0) {
|
|
54973
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "expected_tasks must be a non-empty array");
|
|
54974
|
+
}
|
|
54975
|
+
const expected_tasks = value.expected_tasks.map((item, index) => {
|
|
54976
|
+
const row = strictObject(item, ["task_id", "version"]);
|
|
54977
|
+
const version = row.version;
|
|
54978
|
+
if (!Number.isSafeInteger(version) || Number(version) < 1) {
|
|
54979
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", `expected_tasks.${index}.version must be a positive integer`);
|
|
54980
|
+
}
|
|
54981
|
+
return { task_id: uuid3(row.task_id, `expected_tasks.${index}.task_id`), version: Number(version) };
|
|
54982
|
+
});
|
|
54983
|
+
const ids2 = new Set;
|
|
54984
|
+
for (const row of expected_tasks) {
|
|
54985
|
+
if (ids2.has(row.task_id)) {
|
|
54986
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "expected_tasks must not contain duplicate task IDs");
|
|
54987
|
+
}
|
|
54988
|
+
ids2.add(row.task_id);
|
|
54989
|
+
}
|
|
54990
|
+
if (!Array.isArray(value.shared_plan_splits)) {
|
|
54991
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "shared_plan_splits must be an array");
|
|
54992
|
+
}
|
|
54993
|
+
const shared_plan_splits = value.shared_plan_splits.map((item, index) => {
|
|
54994
|
+
const row = strictObject(item, ["source_plan_id", "destination_plan_id"]);
|
|
54995
|
+
return {
|
|
54996
|
+
source_plan_id: uuid3(row.source_plan_id, `shared_plan_splits.${index}.source_plan_id`),
|
|
54997
|
+
destination_plan_id: uuid3(row.destination_plan_id, `shared_plan_splits.${index}.destination_plan_id`)
|
|
54998
|
+
};
|
|
54999
|
+
});
|
|
55000
|
+
return {
|
|
55001
|
+
...parseTodosTaskSubtreeTransferInspect({
|
|
55002
|
+
source_project_id: value.source_project_id,
|
|
55003
|
+
destination_project_id: value.destination_project_id,
|
|
55004
|
+
destination_task_list_id: value.destination_task_list_id,
|
|
55005
|
+
root_task_id: value.root_task_id,
|
|
55006
|
+
destination_parent_id: value.destination_parent_id
|
|
55007
|
+
}),
|
|
55008
|
+
version: 1,
|
|
55009
|
+
operation_id: boundedString(value.operation_id, "operation_id"),
|
|
55010
|
+
step_id: boundedString(value.step_id, "step_id"),
|
|
55011
|
+
idempotency_key: boundedString(value.idempotency_key, "idempotency_key", 240),
|
|
55012
|
+
precondition_digest: digest2(value.precondition_digest, "precondition_digest"),
|
|
55013
|
+
expected_root_parent_id: nullableUuid(value.expected_root_parent_id, "expected_root_parent_id"),
|
|
55014
|
+
source_population_digest: digest2(value.source_population_digest, "source_population_digest"),
|
|
55015
|
+
expected_tasks,
|
|
55016
|
+
shared_plan_splits
|
|
55017
|
+
};
|
|
55018
|
+
}
|
|
55019
|
+
function parseTodosTaskSubtreeTransferRollback(input) {
|
|
55020
|
+
const value = strictObject(input, [
|
|
55021
|
+
"receipt_id",
|
|
55022
|
+
"operation_id",
|
|
55023
|
+
"step_id",
|
|
55024
|
+
"idempotency_key",
|
|
55025
|
+
"precondition_digest"
|
|
55026
|
+
]);
|
|
55027
|
+
return {
|
|
55028
|
+
receipt_id: uuid3(value.receipt_id, "receipt_id"),
|
|
55029
|
+
operation_id: boundedString(value.operation_id, "operation_id"),
|
|
55030
|
+
step_id: boundedString(value.step_id, "step_id"),
|
|
55031
|
+
idempotency_key: boundedString(value.idempotency_key, "idempotency_key", 240),
|
|
55032
|
+
precondition_digest: digest2(value.precondition_digest, "precondition_digest")
|
|
55033
|
+
};
|
|
55034
|
+
}
|
|
55035
|
+
function deriveTodosTaskSubtreeTransferApplyPreconditionDigest(input) {
|
|
55036
|
+
const {
|
|
55037
|
+
source_project_id,
|
|
55038
|
+
destination_project_id,
|
|
55039
|
+
destination_task_list_id,
|
|
55040
|
+
root_task_id,
|
|
55041
|
+
expected_root_parent_id,
|
|
55042
|
+
destination_parent_id,
|
|
55043
|
+
source_population_digest,
|
|
55044
|
+
expected_tasks,
|
|
55045
|
+
shared_plan_splits
|
|
55046
|
+
} = input;
|
|
55047
|
+
return canonicalDigest({
|
|
55048
|
+
route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
|
|
55049
|
+
direction: "apply",
|
|
55050
|
+
source_project_id,
|
|
55051
|
+
destination_project_id,
|
|
55052
|
+
destination_task_list_id,
|
|
55053
|
+
root_task_id,
|
|
55054
|
+
expected_root_parent_id,
|
|
55055
|
+
destination_parent_id,
|
|
55056
|
+
source_population_digest,
|
|
55057
|
+
expected_tasks,
|
|
55058
|
+
shared_plan_splits
|
|
55059
|
+
});
|
|
55060
|
+
}
|
|
55061
|
+
function resolveTenantId2(value) {
|
|
55062
|
+
const tenant = value ?? "default";
|
|
55063
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$/.test(tenant)) {
|
|
55064
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "tenantId must be a bounded exact authority identifier");
|
|
55065
|
+
}
|
|
55066
|
+
return tenant;
|
|
55067
|
+
}
|
|
55068
|
+
|
|
55069
|
+
class PackageOwnedTodosTaskSubtreeTransferAuthority {
|
|
55070
|
+
backend;
|
|
55071
|
+
tenantId;
|
|
55072
|
+
backendOptions;
|
|
55073
|
+
constructor(backend, options = {}) {
|
|
55074
|
+
this.backend = backend;
|
|
55075
|
+
this.tenantId = resolveTenantId2(options.tenantId);
|
|
55076
|
+
this.backendOptions = { ...options, tenantId: this.tenantId };
|
|
55077
|
+
}
|
|
55078
|
+
async capability() {
|
|
55079
|
+
return {
|
|
55080
|
+
authority: "todos",
|
|
55081
|
+
route: TODOS_TASK_SUBTREE_TRANSFER_ROUTE,
|
|
55082
|
+
schema_version: TODOS_TASK_SUBTREE_TRANSFER_SCHEMA_VERSION,
|
|
55083
|
+
tenant_id: this.tenantId,
|
|
55084
|
+
backend: this.backend.kind,
|
|
55085
|
+
exact_descendant_closure: true,
|
|
55086
|
+
complete_source_population_digest: true,
|
|
55087
|
+
per_task_version_cas: true,
|
|
55088
|
+
explicit_shared_plan_splits: true,
|
|
55089
|
+
atomic_apply: true,
|
|
55090
|
+
immutable_forward_inverse_receipts: true,
|
|
55091
|
+
prior_image_receipts: true,
|
|
55092
|
+
cas_protected_rollback: true,
|
|
55093
|
+
preserves_descendant_parent_ids: true,
|
|
55094
|
+
preserves_task_and_relation_identities: true
|
|
55095
|
+
};
|
|
55096
|
+
}
|
|
55097
|
+
async inspect(input) {
|
|
55098
|
+
return this.backend.inspect(parseTodosTaskSubtreeTransferInspect(input));
|
|
55099
|
+
}
|
|
55100
|
+
async apply(input) {
|
|
55101
|
+
const request = parseTodosTaskSubtreeTransferApply(input);
|
|
55102
|
+
const expected = deriveTodosTaskSubtreeTransferApplyPreconditionDigest(request);
|
|
55103
|
+
if (request.precondition_digest !== expected) {
|
|
55104
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_DIGEST_MISMATCH", "precondition_digest does not match the exact transfer target and source snapshot", { expected_precondition_digest: expected });
|
|
55105
|
+
}
|
|
55106
|
+
const requestDigest = taskSubtreeTransferRequestDigest(request);
|
|
55107
|
+
const expectedKey = deriveTodosTaskSubtreeTransferIdempotencyKey({
|
|
55108
|
+
operation_id: request.operation_id,
|
|
55109
|
+
step_id: request.step_id,
|
|
55110
|
+
direction: "apply",
|
|
55111
|
+
target_selector: request.root_task_id,
|
|
55112
|
+
request_digest: requestDigest,
|
|
55113
|
+
precondition_digest: request.precondition_digest
|
|
55114
|
+
});
|
|
55115
|
+
if (request.idempotency_key !== expectedKey) {
|
|
55116
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_IDEMPOTENCY_MISMATCH", "idempotency_key does not match the exact transfer request", { expected_idempotency_key: expectedKey });
|
|
55117
|
+
}
|
|
55118
|
+
return this.backend.apply(request, this.backendOptions);
|
|
55119
|
+
}
|
|
55120
|
+
async readExact(receiptIdValue) {
|
|
55121
|
+
return this.backend.readExact(uuid3(receiptIdValue, "receipt_id"));
|
|
55122
|
+
}
|
|
55123
|
+
async rollback(input) {
|
|
55124
|
+
const request = parseTodosTaskSubtreeTransferRollback(input);
|
|
55125
|
+
const requestDigest = taskSubtreeTransferRollbackRequestDigest(request);
|
|
55126
|
+
const expectedKey = deriveTodosTaskSubtreeTransferIdempotencyKey({
|
|
55127
|
+
operation_id: request.operation_id,
|
|
55128
|
+
step_id: request.step_id,
|
|
55129
|
+
direction: "rollback",
|
|
55130
|
+
target_selector: request.receipt_id,
|
|
55131
|
+
request_digest: requestDigest,
|
|
55132
|
+
precondition_digest: request.precondition_digest
|
|
55133
|
+
});
|
|
55134
|
+
if (request.idempotency_key !== expectedKey) {
|
|
55135
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_IDEMPOTENCY_MISMATCH", "rollback idempotency_key does not match the exact rollback request", { expected_idempotency_key: expectedKey });
|
|
55136
|
+
}
|
|
55137
|
+
return this.backend.rollback(request, this.backendOptions);
|
|
55138
|
+
}
|
|
55139
|
+
}
|
|
55140
|
+
function createPostgresTodosTaskSubtreeTransferAuthority(client, options = {}) {
|
|
55141
|
+
return new PackageOwnedTodosTaskSubtreeTransferAuthority(new PostgresTodosTaskSubtreeTransferBackend(client, options), options);
|
|
55142
|
+
}
|
|
55143
|
+
var UUID, DIGEST, INSPECT_FIELDS, APPLY_FIELDS;
|
|
55144
|
+
var init_authority3 = __esm(() => {
|
|
55145
|
+
init_canonical2();
|
|
55146
|
+
init_postgres4();
|
|
55147
|
+
init_sqlite3();
|
|
55148
|
+
init_types6();
|
|
55149
|
+
init_canonical2();
|
|
55150
|
+
UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
55151
|
+
DIGEST = /^[0-9a-f]{64}$/i;
|
|
55152
|
+
INSPECT_FIELDS = [
|
|
55153
|
+
"source_project_id",
|
|
55154
|
+
"destination_project_id",
|
|
55155
|
+
"destination_task_list_id",
|
|
55156
|
+
"root_task_id",
|
|
55157
|
+
"destination_parent_id"
|
|
55158
|
+
];
|
|
55159
|
+
APPLY_FIELDS = [
|
|
55160
|
+
...INSPECT_FIELDS,
|
|
55161
|
+
"version",
|
|
55162
|
+
"operation_id",
|
|
55163
|
+
"step_id",
|
|
55164
|
+
"idempotency_key",
|
|
55165
|
+
"precondition_digest",
|
|
55166
|
+
"expected_root_parent_id",
|
|
55167
|
+
"source_population_digest",
|
|
55168
|
+
"expected_tasks",
|
|
55169
|
+
"shared_plan_splits"
|
|
55170
|
+
];
|
|
55171
|
+
});
|
|
55172
|
+
|
|
55173
|
+
// src/task-subtree-transfer/http.ts
|
|
55174
|
+
function json3(body2, status2 = 200) {
|
|
55175
|
+
return new Response(JSON.stringify(body2), { status: status2, headers: JSON_HEADERS3 });
|
|
55176
|
+
}
|
|
55177
|
+
function status2(error) {
|
|
55178
|
+
switch (error.code) {
|
|
55179
|
+
case "TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT":
|
|
55180
|
+
case "TODOS_TASK_SUBTREE_TRANSFER_FOREIGN_REFERENCE":
|
|
55181
|
+
case "TODOS_TASK_SUBTREE_TRANSFER_HIERARCHY_CYCLE":
|
|
55182
|
+
return 400;
|
|
55183
|
+
case "TODOS_TASK_SUBTREE_TRANSFER_RECEIPT_NOT_FOUND":
|
|
55184
|
+
case "TODOS_TASK_SUBTREE_TRANSFER_NOT_FOUND":
|
|
55185
|
+
return 404;
|
|
55186
|
+
case "TODOS_TASK_SUBTREE_TRANSFER_ATOMICITY_UNAVAILABLE":
|
|
55187
|
+
return 503;
|
|
55188
|
+
default:
|
|
55189
|
+
return 409;
|
|
55190
|
+
}
|
|
55191
|
+
}
|
|
55192
|
+
async function boundedText2(message, limit) {
|
|
55193
|
+
if (!message.body)
|
|
55194
|
+
return "";
|
|
55195
|
+
const reader = message.body.getReader();
|
|
55196
|
+
const decoder = new TextDecoder;
|
|
55197
|
+
let bytes = 0;
|
|
55198
|
+
let text2 = "";
|
|
55199
|
+
try {
|
|
55200
|
+
while (true) {
|
|
55201
|
+
const { done, value } = await reader.read();
|
|
55202
|
+
if (done)
|
|
55203
|
+
break;
|
|
55204
|
+
bytes += value.byteLength;
|
|
55205
|
+
if (bytes > limit) {
|
|
55206
|
+
await reader.cancel();
|
|
55207
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "Task-subtree-transfer HTTP body exceeds the byte bound");
|
|
55208
|
+
}
|
|
55209
|
+
text2 += decoder.decode(value, { stream: true });
|
|
55210
|
+
}
|
|
55211
|
+
return text2 + decoder.decode();
|
|
55212
|
+
} finally {
|
|
55213
|
+
reader.releaseLock();
|
|
55214
|
+
}
|
|
55215
|
+
}
|
|
55216
|
+
async function readBody(request) {
|
|
55217
|
+
const declaredLength = Number(request.headers.get("content-length") ?? 0);
|
|
55218
|
+
if (Number.isFinite(declaredLength) && declaredLength > REQUEST_BYTES) {
|
|
55219
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "Task-subtree-transfer request exceeds the byte bound");
|
|
55220
|
+
}
|
|
55221
|
+
const text2 = await boundedText2(request, REQUEST_BYTES);
|
|
55222
|
+
try {
|
|
55223
|
+
return JSON.parse(text2);
|
|
55224
|
+
} catch {
|
|
55225
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "Invalid JSON body");
|
|
55226
|
+
}
|
|
55227
|
+
}
|
|
55228
|
+
async function handleTodosTaskSubtreeTransferHttpRequest(request, url, authority, basePath = "/v1/task-subtree-transfer") {
|
|
55229
|
+
if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`))
|
|
55230
|
+
return null;
|
|
55231
|
+
const action = url.pathname.slice(basePath.length).split("/").filter(Boolean).join("/");
|
|
55232
|
+
try {
|
|
55233
|
+
if ((action === "" || action === "capability") && request.method === "GET") {
|
|
55234
|
+
return json3({ capability: await authority.capability() });
|
|
55235
|
+
}
|
|
55236
|
+
if (request.method !== "POST")
|
|
55237
|
+
return json3({ error: "method not allowed" }, 405);
|
|
55238
|
+
if (action === "inspect")
|
|
55239
|
+
return json3({ inspection: await authority.inspect(await readBody(request)) });
|
|
55240
|
+
if (action === "apply")
|
|
55241
|
+
return json3({ result: await authority.apply(await readBody(request)) }, 201);
|
|
55242
|
+
if (action === "rollback")
|
|
55243
|
+
return json3({ result: await authority.rollback(await readBody(request)) }, 201);
|
|
55244
|
+
if (action === "read-exact") {
|
|
55245
|
+
const input = await readBody(request);
|
|
55246
|
+
if (!input || typeof input.receipt_id !== "string") {
|
|
55247
|
+
throw new TodosTaskSubtreeTransferError("TODOS_TASK_SUBTREE_TRANSFER_INVALID_INPUT", "receipt_id is required");
|
|
55248
|
+
}
|
|
55249
|
+
return json3({ result: await authority.readExact(input.receipt_id) });
|
|
55250
|
+
}
|
|
55251
|
+
return json3({ error: "unknown task-subtree-transfer route" }, 404);
|
|
55252
|
+
} catch (cause) {
|
|
55253
|
+
if (cause instanceof TodosTaskSubtreeTransferError) {
|
|
55254
|
+
return json3({
|
|
55255
|
+
error: cause.message,
|
|
55256
|
+
code: cause.code,
|
|
55257
|
+
details: cause.details,
|
|
55258
|
+
authoritative: true
|
|
55259
|
+
}, status2(cause));
|
|
55260
|
+
}
|
|
55261
|
+
return json3({ error: "internal task-subtree-transfer error" }, 500);
|
|
55262
|
+
}
|
|
55263
|
+
}
|
|
55264
|
+
var JSON_HEADERS3, REQUEST_BYTES, RESPONSE_BYTES;
|
|
55265
|
+
var init_http4 = __esm(() => {
|
|
55266
|
+
init_types6();
|
|
55267
|
+
JSON_HEADERS3 = { "Content-Type": "application/json" };
|
|
55268
|
+
REQUEST_BYTES = 2 * 1024 * 1024;
|
|
55269
|
+
RESPONSE_BYTES = 8 * 1024 * 1024;
|
|
55270
|
+
});
|
|
55271
|
+
|
|
55272
|
+
// src/task-subtree-transfer/index.ts
|
|
55273
|
+
var init_task_subtree_transfer = __esm(() => {
|
|
55274
|
+
init_authority3();
|
|
55275
|
+
init_http4();
|
|
55276
|
+
init_types6();
|
|
55277
|
+
});
|
|
55278
|
+
|
|
54194
55279
|
// src/storage/comment-redaction-backfill.ts
|
|
54195
55280
|
function assertSafeIdentifier2(value) {
|
|
54196
55281
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
|
|
@@ -54288,6 +55373,207 @@ var init_comment_redaction_backfill = __esm(() => {
|
|
|
54288
55373
|
init_postgres_sync();
|
|
54289
55374
|
});
|
|
54290
55375
|
|
|
55376
|
+
// src/storage/timestamp-backfill.ts
|
|
55377
|
+
function toIso(value) {
|
|
55378
|
+
if (!value || value === "null")
|
|
55379
|
+
return null;
|
|
55380
|
+
const parsed = new Date(value);
|
|
55381
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
|
|
55382
|
+
}
|
|
55383
|
+
async function backfillMissingTimestamps(client, options = {}) {
|
|
55384
|
+
const service = options.service ?? "todos";
|
|
55385
|
+
const tableName = options.tableName ?? "todos_sync_records";
|
|
55386
|
+
const batchSize = Math.min(Math.max(options.batchSize ?? 100, 1), 500);
|
|
55387
|
+
const apply = Boolean(options.apply);
|
|
55388
|
+
if (apply && options.confirmation !== TIMESTAMP_BACKFILL_CONFIRMATION) {
|
|
55389
|
+
throw new Error(`timestamp-backfill: --apply requires --confirm=${TIMESTAMP_BACKFILL_CONFIRMATION}`);
|
|
55390
|
+
}
|
|
55391
|
+
if (apply && !options.evidencePath) {
|
|
55392
|
+
throw new Error("timestamp-backfill: --apply requires --evidence-path so the pre-change state is preserved before mutation");
|
|
55393
|
+
}
|
|
55394
|
+
let scanned = 0;
|
|
55395
|
+
let candidates = 0;
|
|
55396
|
+
let completedAtBackfilled = 0;
|
|
55397
|
+
let startedAtBackfilled = 0;
|
|
55398
|
+
let remainingAfterApply = 0;
|
|
55399
|
+
let batches = 0;
|
|
55400
|
+
let cursor = null;
|
|
55401
|
+
for (;; ) {
|
|
55402
|
+
const pageResult = await client.query(`/* todos:timestamp-backfill-candidates */ SELECT object_id,
|
|
55403
|
+
payload->>'status' AS status,
|
|
55404
|
+
payload->>'started_at' AS started_at,
|
|
55405
|
+
payload->>'completed_at' AS completed_at,
|
|
55406
|
+
payload->>'updated_at' AS updated_at,
|
|
55407
|
+
payload->'metadata'->'_failure'->>'failed_at' AS failed_at
|
|
55408
|
+
FROM ${tableName}
|
|
55409
|
+
WHERE service = $1 AND object_type = 'tasks' AND deleted_at IS NULL
|
|
55410
|
+
AND ((payload->>'status' = 'completed'
|
|
55411
|
+
AND ((payload->>'completed_at') IS NULL OR (payload->>'completed_at') = 'null'))
|
|
55412
|
+
OR (payload->>'status' = 'failed'
|
|
55413
|
+
AND ((payload->>'started_at') IS NULL OR (payload->>'started_at') = 'null'
|
|
55414
|
+
OR (payload->>'completed_at') IS NULL OR (payload->>'completed_at') = 'null'))
|
|
55415
|
+
OR (payload->>'status' = 'cancelled'
|
|
55416
|
+
AND ((payload->>'started_at') IS NULL OR (payload->>'started_at') = 'null'
|
|
55417
|
+
OR (payload->>'completed_at') IS NULL OR (payload->>'completed_at') = 'null')))
|
|
55418
|
+
AND ($2::text IS NULL OR object_id > $2)
|
|
55419
|
+
ORDER BY object_id
|
|
55420
|
+
LIMIT $3`, [service, cursor, batchSize]);
|
|
55421
|
+
const page = pageResult.rows;
|
|
55422
|
+
batches += 1;
|
|
55423
|
+
scanned += page.length;
|
|
55424
|
+
if (page.length === 0)
|
|
55425
|
+
break;
|
|
55426
|
+
const batch = page.map((row) => ({
|
|
55427
|
+
...row,
|
|
55428
|
+
started_at: toIso(row.started_at),
|
|
55429
|
+
completed_at: toIso(row.completed_at),
|
|
55430
|
+
updated_at: toIso(row.updated_at),
|
|
55431
|
+
failed_at: toIso(row.failed_at)
|
|
55432
|
+
}));
|
|
55433
|
+
const taskIds = batch.map((row) => row.object_id);
|
|
55434
|
+
const historyResult = await client.query(`/* todos:timestamp-backfill-history */ SELECT
|
|
55435
|
+
payload->>'task_id' AS task_id,
|
|
55436
|
+
payload->>'action' AS action,
|
|
55437
|
+
payload->>'new_value' AS new_value,
|
|
55438
|
+
payload->>'created_at' AS created_at
|
|
55439
|
+
FROM ${tableName}
|
|
55440
|
+
WHERE service = $1 AND object_type = 'audit_history' AND deleted_at IS NULL
|
|
55441
|
+
AND payload->>'action' IN ('start', 'complete', 'fail')
|
|
55442
|
+
AND payload->>'task_id' = ANY($2::text[])`, [service, taskIds]);
|
|
55443
|
+
const historyRows = historyResult.rows;
|
|
55444
|
+
const byTask = new Map;
|
|
55445
|
+
for (const row of historyRows) {
|
|
55446
|
+
if (!row.task_id)
|
|
55447
|
+
continue;
|
|
55448
|
+
const list = byTask.get(row.task_id) ?? [];
|
|
55449
|
+
list.push(row);
|
|
55450
|
+
byTask.set(row.task_id, list);
|
|
55451
|
+
}
|
|
55452
|
+
for (const row of batch) {
|
|
55453
|
+
candidates += 1;
|
|
55454
|
+
const entries = (byTask.get(row.object_id) ?? []).sort((a, b) => (a.created_at ?? "").localeCompare(b.created_at ?? ""));
|
|
55455
|
+
let derivedStartedAt = null;
|
|
55456
|
+
let startedSource = null;
|
|
55457
|
+
if (!row.started_at) {
|
|
55458
|
+
const start = entries.find((entry2) => entry2.action === "start");
|
|
55459
|
+
const candidate = toIso(start?.created_at);
|
|
55460
|
+
if (candidate) {
|
|
55461
|
+
derivedStartedAt = candidate;
|
|
55462
|
+
startedSource = "audit_history";
|
|
55463
|
+
}
|
|
55464
|
+
}
|
|
55465
|
+
let derivedCompletedAt = null;
|
|
55466
|
+
let completedSource = null;
|
|
55467
|
+
if (!row.completed_at) {
|
|
55468
|
+
const terminal = entries.filter((entry2) => entry2.action === "complete" || entry2.action === "fail");
|
|
55469
|
+
if (terminal.length > 0) {
|
|
55470
|
+
const latest = terminal[terminal.length - 1];
|
|
55471
|
+
const candidate = toIso(latest.created_at);
|
|
55472
|
+
if (candidate) {
|
|
55473
|
+
derivedCompletedAt = candidate;
|
|
55474
|
+
completedSource = "audit_history";
|
|
55475
|
+
}
|
|
55476
|
+
}
|
|
55477
|
+
if (!derivedCompletedAt && row.failed_at && row.status === "failed") {
|
|
55478
|
+
const candidate = toIso(row.failed_at);
|
|
55479
|
+
if (candidate) {
|
|
55480
|
+
derivedCompletedAt = candidate;
|
|
55481
|
+
completedSource = "failure_metadata";
|
|
55482
|
+
}
|
|
55483
|
+
}
|
|
55484
|
+
if (!derivedCompletedAt) {
|
|
55485
|
+
const candidate = toIso(row.updated_at);
|
|
55486
|
+
if (candidate) {
|
|
55487
|
+
derivedCompletedAt = candidate;
|
|
55488
|
+
completedSource = "updated_at";
|
|
55489
|
+
}
|
|
55490
|
+
}
|
|
55491
|
+
}
|
|
55492
|
+
const evidenceRow = {
|
|
55493
|
+
object_id: row.object_id,
|
|
55494
|
+
status: row.status,
|
|
55495
|
+
started_at: row.started_at,
|
|
55496
|
+
completed_at: row.completed_at,
|
|
55497
|
+
updated_at: row.updated_at,
|
|
55498
|
+
derived_started_at: derivedStartedAt,
|
|
55499
|
+
derived_completed_at: derivedCompletedAt,
|
|
55500
|
+
started_source: startedSource,
|
|
55501
|
+
completed_source: completedSource
|
|
55502
|
+
};
|
|
55503
|
+
if (apply && options.evidencePath) {
|
|
55504
|
+
const { appendFileSync: appendFileSync2, mkdirSync: mkdirSync8 } = await import("fs");
|
|
55505
|
+
const { dirname: dirname9 } = await import("path");
|
|
55506
|
+
mkdirSync8(dirname9(options.evidencePath), { recursive: true });
|
|
55507
|
+
appendFileSync2(options.evidencePath, JSON.stringify(evidenceRow) + `
|
|
55508
|
+
`);
|
|
55509
|
+
}
|
|
55510
|
+
let completedAtLanded = false;
|
|
55511
|
+
let startedAtLanded = false;
|
|
55512
|
+
if (apply) {
|
|
55513
|
+
const willFillStarted = row.started_at === null && derivedStartedAt !== null;
|
|
55514
|
+
const willFillCompleted = row.completed_at === null && derivedCompletedAt !== null;
|
|
55515
|
+
if (willFillStarted || willFillCompleted) {
|
|
55516
|
+
const result = await client.query(`/* todos:timestamp-backfill-apply */ UPDATE ${tableName} SET
|
|
55517
|
+
payload = jsonb_set(
|
|
55518
|
+
jsonb_set(
|
|
55519
|
+
payload,
|
|
55520
|
+
'{completed_at}',
|
|
55521
|
+
CASE WHEN $3::text IS NOT NULL
|
|
55522
|
+
AND ((payload->>'completed_at') IS NULL OR (payload->>'completed_at') = 'null')
|
|
55523
|
+
THEN to_jsonb($3::text) ELSE payload->'completed_at' END,
|
|
55524
|
+
true
|
|
55525
|
+
),
|
|
55526
|
+
'{started_at}',
|
|
55527
|
+
CASE WHEN $4::text IS NOT NULL
|
|
55528
|
+
AND ((payload->>'started_at') IS NULL OR (payload->>'started_at') = 'null')
|
|
55529
|
+
THEN to_jsonb($4::text) ELSE payload->'started_at' END,
|
|
55530
|
+
true
|
|
55531
|
+
)
|
|
55532
|
+
WHERE service = $1 AND object_type = 'tasks' AND object_id = $2 AND deleted_at IS NULL
|
|
55533
|
+
AND (($3::text IS NOT NULL
|
|
55534
|
+
AND ((payload->>'completed_at') IS NULL OR (payload->>'completed_at') = 'null'))
|
|
55535
|
+
OR ($4::text IS NOT NULL
|
|
55536
|
+
AND ((payload->>'started_at') IS NULL OR (payload->>'started_at') = 'null')))
|
|
55537
|
+
RETURNING object_id,
|
|
55538
|
+
($3::text IS NOT NULL AND payload->>'completed_at' = $3::text) AS completed_written,
|
|
55539
|
+
($4::text IS NOT NULL AND payload->>'started_at' = $4::text) AS started_written`, [service, row.object_id, derivedCompletedAt, derivedStartedAt]);
|
|
55540
|
+
if (result.rows.length > 0) {
|
|
55541
|
+
const landed = result.rows[0];
|
|
55542
|
+
completedAtLanded = willFillCompleted && landed.completed_written;
|
|
55543
|
+
startedAtLanded = willFillStarted && landed.started_written;
|
|
55544
|
+
if (completedAtLanded)
|
|
55545
|
+
completedAtBackfilled += 1;
|
|
55546
|
+
if (startedAtLanded)
|
|
55547
|
+
startedAtBackfilled += 1;
|
|
55548
|
+
}
|
|
55549
|
+
}
|
|
55550
|
+
}
|
|
55551
|
+
if (row.completed_at === null && !completedAtLanded) {
|
|
55552
|
+
remainingAfterApply += 1;
|
|
55553
|
+
} else if ((row.status === "failed" || row.status === "cancelled") && row.started_at === null && !startedAtLanded) {
|
|
55554
|
+
remainingAfterApply += 1;
|
|
55555
|
+
}
|
|
55556
|
+
}
|
|
55557
|
+
const last = batch[batch.length - 1];
|
|
55558
|
+
if (last.object_id === cursor)
|
|
55559
|
+
break;
|
|
55560
|
+
cursor = last.object_id;
|
|
55561
|
+
if (page.length < batchSize)
|
|
55562
|
+
break;
|
|
55563
|
+
}
|
|
55564
|
+
return {
|
|
55565
|
+
dry_run: !apply,
|
|
55566
|
+
scanned,
|
|
55567
|
+
candidates,
|
|
55568
|
+
completed_at_backfilled: apply ? completedAtBackfilled : 0,
|
|
55569
|
+
started_at_backfilled: apply ? startedAtBackfilled : 0,
|
|
55570
|
+
batches,
|
|
55571
|
+
remaining_candidates: apply ? remainingAfterApply : candidates,
|
|
55572
|
+
evidence_path: options.evidencePath ?? null
|
|
55573
|
+
};
|
|
55574
|
+
}
|
|
55575
|
+
var TIMESTAMP_BACKFILL_CONFIRMATION = "BACKFILL_TODOS_TERMINAL_TIMESTAMPS";
|
|
55576
|
+
|
|
54291
55577
|
// src/server/cloud.ts
|
|
54292
55578
|
var exports_cloud = {};
|
|
54293
55579
|
__export(exports_cloud, {
|
|
@@ -54296,8 +55582,8 @@ __export(exports_cloud, {
|
|
|
54296
55582
|
pingCloud: () => pingCloud,
|
|
54297
55583
|
normalizeCloudPayloads: () => normalizeCloudPayloads,
|
|
54298
55584
|
isPostgresBackendConfigured: () => isPostgresBackendConfigured,
|
|
54299
|
-
isCloudModeEnabled: () => isCloudModeEnabled,
|
|
54300
55585
|
getCloudVerifier: () => getCloudVerifier,
|
|
55586
|
+
getCloudTaskSubtreeTransferAuthority: () => getCloudTaskSubtreeTransferAuthority,
|
|
54301
55587
|
getCloudTaskManifestAuthority: () => getCloudTaskManifestAuthority,
|
|
54302
55588
|
getCloudStorageAdapter: () => getCloudStorageAdapter,
|
|
54303
55589
|
getCloudProjectRegistrationAuthority: () => getCloudProjectRegistrationAuthority,
|
|
@@ -54309,6 +55595,7 @@ __export(exports_cloud, {
|
|
|
54309
55595
|
ensureCloudSchema: () => ensureCloudSchema,
|
|
54310
55596
|
ensureCloudCommentCursorIndex: () => ensureCloudCommentCursorIndex,
|
|
54311
55597
|
closeCloud: () => closeCloud,
|
|
55598
|
+
backfillCloudTimestamps: () => backfillCloudTimestamps,
|
|
54312
55599
|
backfillCloudCommentRedaction: () => backfillCloudCommentRedaction,
|
|
54313
55600
|
TODOS_APP_SLUG: () => TODOS_APP_SLUG
|
|
54314
55601
|
});
|
|
@@ -54323,9 +55610,6 @@ function resolveSigningSecret(env = process.env) {
|
|
|
54323
55610
|
function isPostgresBackendConfigured(env = process.env) {
|
|
54324
55611
|
return Boolean(resolveCloudDatabaseUrl(env));
|
|
54325
55612
|
}
|
|
54326
|
-
function isCloudModeEnabled(env = process.env) {
|
|
54327
|
-
return isPostgresBackendConfigured(env);
|
|
54328
|
-
}
|
|
54329
55613
|
function getCloudTenantId() {
|
|
54330
55614
|
return process.env.HASNA_TODOS_TENANT_ID ?? "default";
|
|
54331
55615
|
}
|
|
@@ -54372,6 +55656,15 @@ function getCloudTaskManifestAuthority() {
|
|
|
54372
55656
|
});
|
|
54373
55657
|
return cachedTaskManifestAuthority;
|
|
54374
55658
|
}
|
|
55659
|
+
function getCloudTaskSubtreeTransferAuthority() {
|
|
55660
|
+
if (cachedTaskSubtreeTransferAuthority)
|
|
55661
|
+
return cachedTaskSubtreeTransferAuthority;
|
|
55662
|
+
cachedTaskSubtreeTransferAuthority = createPostgresTodosTaskSubtreeTransferAuthority(getClient(), {
|
|
55663
|
+
service: TODOS_APP_SLUG,
|
|
55664
|
+
tenantId: getCloudTenantId()
|
|
55665
|
+
});
|
|
55666
|
+
return cachedTaskSubtreeTransferAuthority;
|
|
55667
|
+
}
|
|
54375
55668
|
function authClient() {
|
|
54376
55669
|
const client = getClient();
|
|
54377
55670
|
return {
|
|
@@ -54405,7 +55698,7 @@ function getCloudVerifier() {
|
|
|
54405
55698
|
cachedVerifier = verifyApiKey2({
|
|
54406
55699
|
app: TODOS_APP_SLUG,
|
|
54407
55700
|
signingSecret,
|
|
54408
|
-
|
|
55701
|
+
keyStatus: store.keyStatus
|
|
54409
55702
|
});
|
|
54410
55703
|
return cachedVerifier;
|
|
54411
55704
|
}
|
|
@@ -54426,6 +55719,9 @@ async function ensureCloudSchema() {
|
|
|
54426
55719
|
for (const sql of postgresTodosTaskManifestSchemaSql(getCloudTenantId())) {
|
|
54427
55720
|
await client.query(sql);
|
|
54428
55721
|
}
|
|
55722
|
+
for (const sql of postgresTodosTaskSubtreeTransferSchemaSql()) {
|
|
55723
|
+
await client.query(sql);
|
|
55724
|
+
}
|
|
54429
55725
|
await getApiKeyStore().ensureSchema();
|
|
54430
55726
|
})();
|
|
54431
55727
|
return schemaEnsured;
|
|
@@ -54453,6 +55749,9 @@ async function normalizeCloudPayloads() {
|
|
|
54453
55749
|
function backfillCloudCommentRedaction(options = {}) {
|
|
54454
55750
|
return backfillPostgresCommentRedaction(getClient(), { ...options, service: TODOS_APP_SLUG });
|
|
54455
55751
|
}
|
|
55752
|
+
function backfillCloudTimestamps(options = {}) {
|
|
55753
|
+
return backfillMissingTimestamps(getClient(), { ...options, service: TODOS_APP_SLUG });
|
|
55754
|
+
}
|
|
54456
55755
|
async function pingCloud() {
|
|
54457
55756
|
const client = getClient();
|
|
54458
55757
|
const res = await client.query("select 1 as ok");
|
|
@@ -54468,9 +55767,10 @@ async function closeCloud() {
|
|
|
54468
55767
|
cachedPrGroupLedger = null;
|
|
54469
55768
|
cachedProjectRegistrationAuthority = null;
|
|
54470
55769
|
cachedTaskManifestAuthority = null;
|
|
55770
|
+
cachedTaskSubtreeTransferAuthority = null;
|
|
54471
55771
|
schemaEnsured = null;
|
|
54472
55772
|
}
|
|
54473
|
-
var TODOS_APP_SLUG = "todos", cachedClient = null, cachedAdapter = null, cachedStore = null, cachedVerifier = null, cachedPrGroupLedger = null, cachedProjectRegistrationAuthority = null, cachedTaskManifestAuthority = null, schemaEnsured = null;
|
|
55773
|
+
var TODOS_APP_SLUG = "todos", cachedClient = null, cachedAdapter = null, cachedStore = null, cachedVerifier = null, cachedPrGroupLedger = null, cachedProjectRegistrationAuthority = null, cachedTaskManifestAuthority = null, cachedTaskSubtreeTransferAuthority = null, schemaEnsured = null;
|
|
54474
55774
|
var init_cloud = __esm(() => {
|
|
54475
55775
|
init_cloud_client();
|
|
54476
55776
|
init_postgres_adapter();
|
|
@@ -54478,6 +55778,7 @@ var init_cloud = __esm(() => {
|
|
|
54478
55778
|
init_postgres();
|
|
54479
55779
|
init_project_registration();
|
|
54480
55780
|
init_task_manifest();
|
|
55781
|
+
init_task_subtree_transfer();
|
|
54481
55782
|
init_postgres_sync();
|
|
54482
55783
|
init_comment_redaction_backfill();
|
|
54483
55784
|
});
|
|
@@ -54691,8 +55992,8 @@ function rowToOrg(row) {
|
|
|
54691
55992
|
function createOrg(input, db) {
|
|
54692
55993
|
const d = db || getDatabase();
|
|
54693
55994
|
const id = uuid();
|
|
54694
|
-
const
|
|
54695
|
-
d.run(`INSERT INTO orgs (id, name, description, metadata, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`, [id, input.name, input.description || null, JSON.stringify(input.metadata || {}),
|
|
55995
|
+
const timestamp6 = now();
|
|
55996
|
+
d.run(`INSERT INTO orgs (id, name, description, metadata, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`, [id, input.name, input.description || null, JSON.stringify(input.metadata || {}), timestamp6, timestamp6]);
|
|
54696
55997
|
return getOrg(id, d);
|
|
54697
55998
|
}
|
|
54698
55999
|
function getOrg(id, db) {
|
|
@@ -54836,9 +56137,9 @@ function parseBoundedLimit(value, fallback, max) {
|
|
|
54836
56137
|
return fallback;
|
|
54837
56138
|
return Math.min(parsed, max);
|
|
54838
56139
|
}
|
|
54839
|
-
function mapTaskError(e,
|
|
56140
|
+
function mapTaskError(e, json5) {
|
|
54840
56141
|
if (e instanceof VersionConflictError) {
|
|
54841
|
-
return
|
|
56142
|
+
return json5({
|
|
54842
56143
|
error: e.message,
|
|
54843
56144
|
code: VersionConflictError.code,
|
|
54844
56145
|
expected_version: e.expectedVersion,
|
|
@@ -54846,23 +56147,23 @@ function mapTaskError(e, json4) {
|
|
|
54846
56147
|
}, 409);
|
|
54847
56148
|
}
|
|
54848
56149
|
if (e instanceof TaskNotFoundError) {
|
|
54849
|
-
return
|
|
56150
|
+
return json5({ error: e.message, code: TaskNotFoundError.code }, 404);
|
|
54850
56151
|
}
|
|
54851
56152
|
if (e instanceof LockError) {
|
|
54852
|
-
return
|
|
56153
|
+
return json5({ error: e.message, code: LockError.code }, 409);
|
|
54853
56154
|
}
|
|
54854
56155
|
if (e instanceof CompletionGuardError) {
|
|
54855
|
-
return
|
|
56156
|
+
return json5({
|
|
54856
56157
|
error: e.message,
|
|
54857
56158
|
code: CompletionGuardError.code,
|
|
54858
56159
|
retry_after: e.retryAfterSeconds ?? null
|
|
54859
56160
|
}, 409);
|
|
54860
56161
|
}
|
|
54861
56162
|
if (e instanceof TaskNotStartableError) {
|
|
54862
|
-
return
|
|
56163
|
+
return json5({ error: e.message, code: TaskNotStartableError.code }, 409);
|
|
54863
56164
|
}
|
|
54864
56165
|
if (e instanceof Error && / is blocked by /.test(e.message)) {
|
|
54865
|
-
return
|
|
56166
|
+
return json5({ error: e.message, code: "TASK_NOT_STARTABLE" }, 409);
|
|
54866
56167
|
}
|
|
54867
56168
|
return null;
|
|
54868
56169
|
}
|
|
@@ -54947,11 +56248,11 @@ data: ${JSON.stringify({ type: "connected", agent_id: agentId, timestamp: new Da
|
|
|
54947
56248
|
}
|
|
54948
56249
|
});
|
|
54949
56250
|
}
|
|
54950
|
-
function handleHealth(_ctx,
|
|
56251
|
+
function handleHealth(_ctx, json5) {
|
|
54951
56252
|
const stats2 = getTaskStats();
|
|
54952
56253
|
const staleCount = getStaleTasks(30).length;
|
|
54953
56254
|
const overdueRecurring = getOverdueTasks().filter((t) => t.recurrence_rule && t.status === "pending").length;
|
|
54954
|
-
return
|
|
56255
|
+
return json5({
|
|
54955
56256
|
status: staleCount === 0 && overdueRecurring === 0 ? "ok" : "warn",
|
|
54956
56257
|
tasks: stats2.total,
|
|
54957
56258
|
stale: staleCount,
|
|
@@ -54959,18 +56260,18 @@ function handleHealth(_ctx, json4) {
|
|
|
54959
56260
|
timestamp: new Date().toISOString()
|
|
54960
56261
|
});
|
|
54961
56262
|
}
|
|
54962
|
-
function handleHeadlessBoundary(_ctx,
|
|
56263
|
+
function handleHeadlessBoundary(_ctx, json5) {
|
|
54963
56264
|
const { getHeadlessBoundaryManifest: getHeadlessBoundaryManifest2 } = (init_headless_boundaries(), __toCommonJS(exports_headless_boundaries));
|
|
54964
|
-
return
|
|
56265
|
+
return json5(getHeadlessBoundaryManifest2());
|
|
54965
56266
|
}
|
|
54966
|
-
function handleStats(_ctx,
|
|
56267
|
+
function handleStats(_ctx, json5) {
|
|
54967
56268
|
const stats2 = getTaskStats();
|
|
54968
56269
|
const byStatus = stats2.by_status;
|
|
54969
56270
|
const projects = listProjects();
|
|
54970
56271
|
const agents = listAgents();
|
|
54971
56272
|
const staleCount = getStaleTasks(30).length;
|
|
54972
56273
|
const overdueRecurring = getOverdueTasks().filter((t) => t.recurrence_rule && t.status === "pending").length;
|
|
54973
|
-
return
|
|
56274
|
+
return json5({
|
|
54974
56275
|
total_tasks: stats2.total,
|
|
54975
56276
|
pending: byStatus["pending"] ?? 0,
|
|
54976
56277
|
in_progress: byStatus["in_progress"] ?? 0,
|
|
@@ -54993,10 +56294,10 @@ function taskStatusQueryParam(url) {
|
|
|
54993
56294
|
return { ok: false, message: result.message };
|
|
54994
56295
|
return { ok: true, value: collapseEnumValues(result.values) };
|
|
54995
56296
|
}
|
|
54996
|
-
async function handleListTasks(_req, url, _ctx,
|
|
56297
|
+
async function handleListTasks(_req, url, _ctx, json5, taskToSummary2) {
|
|
54997
56298
|
const statusParam = taskStatusQueryParam(url);
|
|
54998
56299
|
if (!statusParam.ok)
|
|
54999
|
-
return
|
|
56300
|
+
return json5({ error: statusParam.message }, 400);
|
|
55000
56301
|
const projectId = url.searchParams.get("project_id") || undefined;
|
|
55001
56302
|
const sessionId = url.searchParams.get("session_id") || undefined;
|
|
55002
56303
|
const agentId = url.searchParams.get("agent_id") || undefined;
|
|
@@ -55011,13 +56312,13 @@ async function handleListTasks(_req, url, _ctx, json4, taskToSummary2) {
|
|
|
55011
56312
|
limit: limitParam ? parseInt(limitParam, 10) : undefined,
|
|
55012
56313
|
offset: offsetParam ? parseInt(offsetParam, 10) : undefined
|
|
55013
56314
|
});
|
|
55014
|
-
return
|
|
56315
|
+
return json5(tasks.map((t) => taskToSummary2(t, fields)));
|
|
55015
56316
|
}
|
|
55016
|
-
async function handleCreateTask(req, ctx,
|
|
56317
|
+
async function handleCreateTask(req, ctx, json5, taskToSummary2) {
|
|
55017
56318
|
try {
|
|
55018
56319
|
const body2 = await req.json();
|
|
55019
56320
|
if (!body2.title)
|
|
55020
|
-
return
|
|
56321
|
+
return json5({ error: "Missing 'title'" }, 400);
|
|
55021
56322
|
const createdBy = body2.created_by ?? body2.agent_id ?? "dashboard";
|
|
55022
56323
|
const task3 = createTask({
|
|
55023
56324
|
title: body2.title,
|
|
@@ -55029,19 +56330,19 @@ async function handleCreateTask(req, ctx, json4, taskToSummary2) {
|
|
|
55029
56330
|
...body2.assigned_to ? { assigned_to: body2.assigned_to } : {}
|
|
55030
56331
|
});
|
|
55031
56332
|
ctx.broadcastEvent({ type: "task", task_id: task3.id, action: "created", agent_id: task3.agent_id, project_id: task3.project_id });
|
|
55032
|
-
return
|
|
56333
|
+
return json5(taskToSummary2(task3), 201);
|
|
55033
56334
|
} catch (e) {
|
|
55034
|
-
return
|
|
56335
|
+
return json5({ error: e instanceof Error ? e.message : "Failed to create task" }, 500);
|
|
55035
56336
|
}
|
|
55036
56337
|
}
|
|
55037
|
-
async function handleUpsertTask(req, ctx,
|
|
56338
|
+
async function handleUpsertTask(req, ctx, json5, taskToSummary2) {
|
|
55038
56339
|
try {
|
|
55039
56340
|
const body2 = await req.json();
|
|
55040
56341
|
if (typeof body2["fingerprint"] !== "string" || body2["fingerprint"].trim() === "") {
|
|
55041
|
-
return
|
|
56342
|
+
return json5({ error: "Missing 'fingerprint'" }, 400);
|
|
55042
56343
|
}
|
|
55043
56344
|
if (typeof body2["title"] !== "string" || body2["title"].trim() === "") {
|
|
55044
|
-
return
|
|
56345
|
+
return json5({ error: "Missing 'title'" }, 400);
|
|
55045
56346
|
}
|
|
55046
56347
|
const metadata = body2["metadata"] && typeof body2["metadata"] === "object" && !Array.isArray(body2["metadata"]) ? { ...body2["metadata"] } : {};
|
|
55047
56348
|
for (const key2 of ["expectation_id", "expectation_fingerprint", "evidence_paths", "origin_loop_id", "origin_run_id", "expected", "observed", "acceptance"]) {
|
|
@@ -55062,9 +56363,9 @@ async function handleUpsertTask(req, ctx, json4, taskToSummary2) {
|
|
|
55062
56363
|
metadata
|
|
55063
56364
|
});
|
|
55064
56365
|
ctx.broadcastEvent({ type: "task", task_id: result.task.id, action: result.created ? "created" : "updated", agent_id: result.task.agent_id, project_id: result.task.project_id });
|
|
55065
|
-
return
|
|
56366
|
+
return json5({ created: result.created, task: taskToSummary2(result.task) }, result.created ? 201 : 200);
|
|
55066
56367
|
} catch (e) {
|
|
55067
|
-
return
|
|
56368
|
+
return json5({ error: e instanceof Error ? e.message : "Failed to upsert task" }, 500);
|
|
55068
56369
|
}
|
|
55069
56370
|
}
|
|
55070
56371
|
function handleTasksExport(_req, url, _ctx, _json, taskToSummary2) {
|
|
@@ -55116,11 +56417,11 @@ function handleTasksExport(_req, url, _ctx, _json, taskToSummary2) {
|
|
|
55116
56417
|
}
|
|
55117
56418
|
});
|
|
55118
56419
|
}
|
|
55119
|
-
async function handleTasksBulk(req, _ctx,
|
|
56420
|
+
async function handleTasksBulk(req, _ctx, json5) {
|
|
55120
56421
|
try {
|
|
55121
56422
|
const body2 = await req.json();
|
|
55122
56423
|
if (!body2.ids?.length || !body2.action)
|
|
55123
|
-
return
|
|
56424
|
+
return json5({ error: "Missing ids or action" }, 400);
|
|
55124
56425
|
const results = [];
|
|
55125
56426
|
for (const id of body2.ids) {
|
|
55126
56427
|
try {
|
|
@@ -55138,84 +56439,84 @@ async function handleTasksBulk(req, _ctx, json4) {
|
|
|
55138
56439
|
results.push({ id, success: false, error: e instanceof Error ? e.message : "Failed" });
|
|
55139
56440
|
}
|
|
55140
56441
|
}
|
|
55141
|
-
return
|
|
56442
|
+
return json5({ results, succeeded: results.filter((r) => r.success).length, failed: results.filter((r) => !r.success).length });
|
|
55142
56443
|
} catch (e) {
|
|
55143
|
-
return
|
|
56444
|
+
return json5({ error: e instanceof Error ? e.message : "Failed" }, 500);
|
|
55144
56445
|
}
|
|
55145
56446
|
}
|
|
55146
|
-
function handleTasksStatus(_req, url, _ctx,
|
|
56447
|
+
function handleTasksStatus(_req, url, _ctx, json5) {
|
|
55147
56448
|
try {
|
|
55148
56449
|
const projectId = url.searchParams.get("project_id") || undefined;
|
|
55149
56450
|
const agentId = url.searchParams.get("agent_id") || undefined;
|
|
55150
|
-
const
|
|
55151
|
-
return
|
|
56451
|
+
const status3 = getStatus(projectId ? { project_id: projectId } : undefined, agentId);
|
|
56452
|
+
return json5(status3);
|
|
55152
56453
|
} catch (e) {
|
|
55153
|
-
return
|
|
56454
|
+
return json5({ error: e instanceof Error ? e.message : "Failed" }, 500);
|
|
55154
56455
|
}
|
|
55155
56456
|
}
|
|
55156
|
-
function handleTasksNext(_req, url, _ctx,
|
|
56457
|
+
function handleTasksNext(_req, url, _ctx, json5, taskToSummary2) {
|
|
55157
56458
|
try {
|
|
55158
56459
|
const projectId = url.searchParams.get("project_id") || undefined;
|
|
55159
56460
|
const agentId = url.searchParams.get("agent_id") || undefined;
|
|
55160
56461
|
const fields = parseFieldsParam(url);
|
|
55161
56462
|
const task3 = getNextTask(agentId, projectId ? { project_id: projectId } : undefined);
|
|
55162
|
-
return
|
|
56463
|
+
return json5({ task: task3 ? taskToSummary2(task3, fields) : null });
|
|
55163
56464
|
} catch (e) {
|
|
55164
|
-
return
|
|
56465
|
+
return json5({ error: e instanceof Error ? e.message : "Failed" }, 500);
|
|
55165
56466
|
}
|
|
55166
56467
|
}
|
|
55167
|
-
function handleTasksActive(_req, url, _ctx,
|
|
56468
|
+
function handleTasksActive(_req, url, _ctx, json5) {
|
|
55168
56469
|
try {
|
|
55169
56470
|
const projectId = url.searchParams.get("project_id") || undefined;
|
|
55170
56471
|
const work = getActiveWork(projectId ? { project_id: projectId } : undefined);
|
|
55171
|
-
return
|
|
56472
|
+
return json5({ active: work, count: work.length });
|
|
55172
56473
|
} catch (e) {
|
|
55173
|
-
return
|
|
56474
|
+
return json5({ error: e instanceof Error ? e.message : "Failed" }, 500);
|
|
55174
56475
|
}
|
|
55175
56476
|
}
|
|
55176
|
-
function handleTasksStale(_req, url, _ctx,
|
|
56477
|
+
function handleTasksStale(_req, url, _ctx, json5, taskToSummary2) {
|
|
55177
56478
|
try {
|
|
55178
56479
|
const projectId = url.searchParams.get("project_id") || undefined;
|
|
55179
56480
|
const minutes2 = parseInt(url.searchParams.get("minutes") || "30", 10);
|
|
55180
56481
|
const fields = parseFieldsParam(url);
|
|
55181
56482
|
const tasks = getStaleTasks(minutes2, projectId ? { project_id: projectId } : undefined);
|
|
55182
|
-
return
|
|
56483
|
+
return json5({ tasks: tasks.map((t) => taskToSummary2(t, fields)), count: tasks.length });
|
|
55183
56484
|
} catch (e) {
|
|
55184
|
-
return
|
|
56485
|
+
return json5({ error: e instanceof Error ? e.message : "Failed" }, 500);
|
|
55185
56486
|
}
|
|
55186
56487
|
}
|
|
55187
|
-
function handleTasksChanged(_req, url, _ctx,
|
|
56488
|
+
function handleTasksChanged(_req, url, _ctx, json5, taskToSummary2) {
|
|
55188
56489
|
try {
|
|
55189
56490
|
const since = url.searchParams.get("since");
|
|
55190
56491
|
if (!since)
|
|
55191
|
-
return
|
|
56492
|
+
return json5({ error: "since parameter required (ISO date string)" }, 400);
|
|
55192
56493
|
const projectId = url.searchParams.get("project_id") || undefined;
|
|
55193
56494
|
const fields = parseFieldsParam(url);
|
|
55194
56495
|
const tasks = getTasksChangedSince(since, projectId ? { project_id: projectId } : undefined);
|
|
55195
|
-
return
|
|
56496
|
+
return json5({ tasks: tasks.map((t) => taskToSummary2(t, fields)), count: tasks.length, since });
|
|
55196
56497
|
} catch (e) {
|
|
55197
|
-
return
|
|
56498
|
+
return json5({ error: e instanceof Error ? e.message : "Failed" }, 500);
|
|
55198
56499
|
}
|
|
55199
56500
|
}
|
|
55200
|
-
function handleTasksContext(_req, url, _ctx,
|
|
56501
|
+
function handleTasksContext(_req, url, _ctx, json5, taskToSummary2) {
|
|
55201
56502
|
const agentId = url.searchParams.get("agent_id") || undefined;
|
|
55202
56503
|
const projectId = url.searchParams.get("project_id") || undefined;
|
|
55203
56504
|
const format = url.searchParams.get("format") || "text";
|
|
55204
56505
|
const fields = parseFieldsParam(url);
|
|
55205
56506
|
const filters = projectId ? { project_id: projectId } : undefined;
|
|
55206
|
-
const
|
|
56507
|
+
const status3 = getStatus(filters, agentId);
|
|
55207
56508
|
const next = getNextTask(agentId, filters);
|
|
55208
56509
|
if (format === "json") {
|
|
55209
|
-
return
|
|
56510
|
+
return json5({ status: status3, next_task: next ? taskToSummary2(next, fields) : null });
|
|
55210
56511
|
}
|
|
55211
56512
|
const lines = [];
|
|
55212
|
-
lines.push(`Tasks: ${
|
|
55213
|
-
if (
|
|
55214
|
-
lines.push(`${
|
|
55215
|
-
if (
|
|
55216
|
-
lines.push(`${
|
|
55217
|
-
if (
|
|
55218
|
-
lines.push(`Active: ${
|
|
56513
|
+
lines.push(`Tasks: ${status3.pending} pending | ${status3.in_progress} active | ${status3.completed} done`);
|
|
56514
|
+
if (status3.stale_count > 0)
|
|
56515
|
+
lines.push(`${status3.stale_count} stale tasks stuck in-progress`);
|
|
56516
|
+
if (status3.overdue_recurring > 0)
|
|
56517
|
+
lines.push(`${status3.overdue_recurring} overdue recurring tasks`);
|
|
56518
|
+
if (status3.active_work.length > 0) {
|
|
56519
|
+
lines.push(`Active: ${status3.active_work.slice(0, 3).map((w) => `${w.short_id || w.id.slice(0, 8)} (${w.assigned_to || "?"})`).join(", ")}`);
|
|
55219
56520
|
}
|
|
55220
56521
|
if (next)
|
|
55221
56522
|
lines.push(`Next up: ${next.short_id || next.id.slice(0, 8)} [${next.priority}] ${next.title}`);
|
|
@@ -55223,18 +56524,18 @@ function handleTasksContext(_req, url, _ctx, json4, taskToSummary2) {
|
|
|
55223
56524
|
`);
|
|
55224
56525
|
return new Response(text2, { headers: { "Content-Type": "text/plain" } });
|
|
55225
56526
|
}
|
|
55226
|
-
function handleTaskAttachments(id, _ctx,
|
|
56527
|
+
function handleTaskAttachments(id, _ctx, json5) {
|
|
55227
56528
|
const task3 = getTask(id);
|
|
55228
56529
|
if (!task3)
|
|
55229
|
-
return
|
|
56530
|
+
return json5({ error: "Task not found" }, 404);
|
|
55230
56531
|
const evidence = task3.metadata?._evidence || {};
|
|
55231
56532
|
const attachmentIds = evidence.attachments || [];
|
|
55232
|
-
return
|
|
56533
|
+
return json5({ task_id: id, short_id: task3.short_id, attachment_ids: attachmentIds, count: attachmentIds.length, files_changed: evidence.files_changed, commit_hash: evidence.commit_hash, notes: evidence.notes });
|
|
55233
56534
|
}
|
|
55234
|
-
async function handleTaskProgress(id, req, method, _ctx,
|
|
56535
|
+
async function handleTaskProgress(id, req, method, _ctx, json5, url) {
|
|
55235
56536
|
const task3 = getTask(id);
|
|
55236
56537
|
if (!task3)
|
|
55237
|
-
return
|
|
56538
|
+
return json5({ error: "Task not found" }, 404);
|
|
55238
56539
|
if (method === "GET") {
|
|
55239
56540
|
const all = listComments(id);
|
|
55240
56541
|
const progress = all.filter((c) => c.type === "progress");
|
|
@@ -55242,7 +56543,7 @@ async function handleTaskProgress(id, req, method, _ctx, json4, url) {
|
|
|
55242
56543
|
const format = url?.searchParams.get("format") || "compact";
|
|
55243
56544
|
const limit = parseBoundedLimit(url?.searchParams.get("limit") || null, 20, 200);
|
|
55244
56545
|
const progressEntries = format === "full" ? progress : progress.slice(-limit);
|
|
55245
|
-
return
|
|
56546
|
+
return json5({
|
|
55246
56547
|
task_id: id,
|
|
55247
56548
|
progress_entries: progressEntries,
|
|
55248
56549
|
latest,
|
|
@@ -55259,27 +56560,27 @@ async function handleTaskProgress(id, req, method, _ctx, json4, url) {
|
|
|
55259
56560
|
try {
|
|
55260
56561
|
const body2 = await req.json();
|
|
55261
56562
|
if (!body2.message)
|
|
55262
|
-
return
|
|
56563
|
+
return json5({ error: "message required" }, 400);
|
|
55263
56564
|
const comment2 = logProgress(id, body2.message, body2.pct_complete, body2.agent_id);
|
|
55264
|
-
return
|
|
56565
|
+
return json5(comment2, 201);
|
|
55265
56566
|
} catch (e) {
|
|
55266
|
-
return
|
|
56567
|
+
return json5({ error: e instanceof Error ? e.message : "Failed to log progress" }, 500);
|
|
55267
56568
|
}
|
|
55268
56569
|
}
|
|
55269
56570
|
return null;
|
|
55270
56571
|
}
|
|
55271
|
-
function handleGetTask(id, _ctx,
|
|
56572
|
+
function handleGetTask(id, _ctx, json5, taskToSummary2, url) {
|
|
55272
56573
|
const task3 = getTask(id);
|
|
55273
56574
|
if (!task3)
|
|
55274
|
-
return
|
|
55275
|
-
return
|
|
56575
|
+
return json5({ error: "Task not found" }, 404);
|
|
56576
|
+
return json5(taskToSummary2(task3, url ? parseFieldsParam(url) : undefined));
|
|
55276
56577
|
}
|
|
55277
|
-
async function handlePatchTask(id, req, _ctx,
|
|
56578
|
+
async function handlePatchTask(id, req, _ctx, json5, taskToSummary2) {
|
|
55278
56579
|
try {
|
|
55279
56580
|
const body2 = await req.json();
|
|
55280
56581
|
const task3 = getTask(id);
|
|
55281
56582
|
if (!task3)
|
|
55282
|
-
return
|
|
56583
|
+
return json5({ error: "Task not found" }, 404);
|
|
55283
56584
|
const ALLOWED = new Set(["title", "description", "status", "priority", "assigned_to", "plan_id", "task_list_id", "tags", "metadata", "due_at", "estimated_minutes", "actual_minutes", "confidence", "retry_count", "max_retries", "retry_after", "task_type"]);
|
|
55284
56585
|
const safeBody = {};
|
|
55285
56586
|
for (const [key2, value] of Object.entries(body2)) {
|
|
@@ -55291,85 +56592,85 @@ async function handlePatchTask(id, req, _ctx, json4, taskToSummary2) {
|
|
|
55291
56592
|
...safeBody,
|
|
55292
56593
|
version: clientVersion
|
|
55293
56594
|
});
|
|
55294
|
-
return
|
|
56595
|
+
return json5(taskToSummary2(updated));
|
|
55295
56596
|
} catch (e) {
|
|
55296
|
-
const mapped = mapTaskError(e,
|
|
56597
|
+
const mapped = mapTaskError(e, json5);
|
|
55297
56598
|
if (mapped)
|
|
55298
56599
|
return mapped;
|
|
55299
|
-
return
|
|
56600
|
+
return json5({ error: e instanceof Error ? e.message : "Failed to update task" }, 500);
|
|
55300
56601
|
}
|
|
55301
56602
|
}
|
|
55302
|
-
function handleDeleteTask(id, _ctx,
|
|
56603
|
+
function handleDeleteTask(id, _ctx, json5) {
|
|
55303
56604
|
const deleted = deleteTask(id);
|
|
55304
56605
|
if (!deleted)
|
|
55305
|
-
return
|
|
55306
|
-
return
|
|
56606
|
+
return json5({ error: "Task not found" }, 404);
|
|
56607
|
+
return json5({ success: true });
|
|
55307
56608
|
}
|
|
55308
|
-
function handleStartTask(id, ctx,
|
|
56609
|
+
function handleStartTask(id, ctx, json5, taskToSummary2) {
|
|
55309
56610
|
try {
|
|
55310
56611
|
const task3 = startTask(id, "dashboard");
|
|
55311
56612
|
ctx.broadcastEvent({ type: "task", task_id: task3.id, action: "started", agent_id: "dashboard", project_id: task3.project_id });
|
|
55312
|
-
return
|
|
56613
|
+
return json5(taskToSummary2(task3));
|
|
55313
56614
|
} catch (e) {
|
|
55314
|
-
const mapped = mapTaskError(e,
|
|
56615
|
+
const mapped = mapTaskError(e, json5);
|
|
55315
56616
|
if (mapped)
|
|
55316
56617
|
return mapped;
|
|
55317
|
-
return
|
|
56618
|
+
return json5({ error: e instanceof Error ? e.message : "Failed to start task" }, 500);
|
|
55318
56619
|
}
|
|
55319
56620
|
}
|
|
55320
|
-
async function handleFailTask(id, req, ctx,
|
|
56621
|
+
async function handleFailTask(id, req, ctx, json5, taskToSummary2) {
|
|
55321
56622
|
try {
|
|
55322
56623
|
const body2 = await req.json().catch(() => ({}));
|
|
55323
56624
|
const result = failTask(id, body2.agent_id, body2.reason, { retry: body2.retry, error_code: body2.error_code });
|
|
55324
56625
|
ctx.broadcastEvent({ type: "task", task_id: id, action: "failed", agent_id: body2.agent_id || null, project_id: result.task.project_id });
|
|
55325
|
-
return
|
|
56626
|
+
return json5({ task: taskToSummary2(result.task), retry_task: result.retryTask ? taskToSummary2(result.retryTask) : null });
|
|
55326
56627
|
} catch (e) {
|
|
55327
|
-
return
|
|
56628
|
+
return json5({ error: e instanceof Error ? e.message : "Failed to fail task" }, 500);
|
|
55328
56629
|
}
|
|
55329
56630
|
}
|
|
55330
|
-
function handleCompleteTask(id, ctx,
|
|
56631
|
+
function handleCompleteTask(id, ctx, json5, taskToSummary2) {
|
|
55331
56632
|
try {
|
|
55332
56633
|
const task3 = completeTask(id, "dashboard");
|
|
55333
56634
|
ctx.broadcastEvent({ type: "task", task_id: task3.id, action: "completed", agent_id: "dashboard", project_id: task3.project_id });
|
|
55334
|
-
return
|
|
56635
|
+
return json5(taskToSummary2(task3));
|
|
55335
56636
|
} catch (e) {
|
|
55336
|
-
const mapped = mapTaskError(e,
|
|
56637
|
+
const mapped = mapTaskError(e, json5);
|
|
55337
56638
|
if (mapped)
|
|
55338
56639
|
return mapped;
|
|
55339
|
-
return
|
|
56640
|
+
return json5({ error: e instanceof Error ? e.message : "Failed to complete task" }, 500);
|
|
55340
56641
|
}
|
|
55341
56642
|
}
|
|
55342
|
-
function handleListProjects(url, _ctx,
|
|
56643
|
+
function handleListProjects(url, _ctx, json5) {
|
|
55343
56644
|
const pFieldsParam = url.searchParams.get("fields");
|
|
55344
56645
|
const pFields = pFieldsParam ? pFieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
|
|
55345
56646
|
const projects = listProjects();
|
|
55346
|
-
return
|
|
56647
|
+
return json5(pFields ? projects.map((p) => Object.fromEntries(pFields.map((f) => [f, p[f] ?? null]))) : projects);
|
|
55347
56648
|
}
|
|
55348
|
-
async function handleCreateProject(req, _ctx,
|
|
56649
|
+
async function handleCreateProject(req, _ctx, json5) {
|
|
55349
56650
|
try {
|
|
55350
56651
|
const body2 = await req.json();
|
|
55351
56652
|
if (!body2.name || !body2.path)
|
|
55352
|
-
return
|
|
56653
|
+
return json5({ error: "Missing name or path" }, 400);
|
|
55353
56654
|
const project = createProject({ name: body2.name, path: body2.path, description: body2.description });
|
|
55354
|
-
return
|
|
56655
|
+
return json5(project, 201);
|
|
55355
56656
|
} catch (e) {
|
|
55356
|
-
return
|
|
56657
|
+
return json5({ error: e instanceof Error ? e.message : "Failed to create project" }, 500);
|
|
55357
56658
|
}
|
|
55358
56659
|
}
|
|
55359
|
-
function handleDeleteProject(id, _ctx,
|
|
56660
|
+
function handleDeleteProject(id, _ctx, json5) {
|
|
55360
56661
|
const deleted = deleteProject(id);
|
|
55361
56662
|
if (!deleted)
|
|
55362
|
-
return
|
|
55363
|
-
return
|
|
56663
|
+
return json5({ error: "Project not found" }, 404);
|
|
56664
|
+
return json5({ success: true });
|
|
55364
56665
|
}
|
|
55365
|
-
async function handleAgentMe(_req, url, _ctx,
|
|
56666
|
+
async function handleAgentMe(_req, url, _ctx, json5, taskToSummary2) {
|
|
55366
56667
|
try {
|
|
55367
56668
|
const name = url.searchParams.get("name");
|
|
55368
56669
|
if (!name)
|
|
55369
|
-
return
|
|
56670
|
+
return json5({ error: "Missing name param" }, 400);
|
|
55370
56671
|
const agentResult = registerAgent({ name });
|
|
55371
56672
|
if (isAgentConflict(agentResult))
|
|
55372
|
-
return
|
|
56673
|
+
return json5({ error: agentResult.message, conflict: true }, 409);
|
|
55373
56674
|
const agent = agentResult;
|
|
55374
56675
|
const tasks = listTasks({ assigned_to: agent.name });
|
|
55375
56676
|
const agentIdTasks = listTasks({ agent_id: agent.id });
|
|
@@ -55377,7 +56678,7 @@ async function handleAgentMe(_req, url, _ctx, json4, taskToSummary2) {
|
|
|
55377
56678
|
const pending = allTasks.filter((t) => t.status === "pending");
|
|
55378
56679
|
const inProgress = allTasks.filter((t) => t.status === "in_progress");
|
|
55379
56680
|
const completed = allTasks.filter((t) => t.status === "completed");
|
|
55380
|
-
return
|
|
56681
|
+
return json5({
|
|
55381
56682
|
agent,
|
|
55382
56683
|
pending_tasks: pending.map((t) => taskToSummary2(t)),
|
|
55383
56684
|
in_progress_tasks: inProgress.map((t) => taskToSummary2(t)),
|
|
@@ -55391,132 +56692,132 @@ async function handleAgentMe(_req, url, _ctx, json4, taskToSummary2) {
|
|
|
55391
56692
|
});
|
|
55392
56693
|
} catch (e) {
|
|
55393
56694
|
if (e instanceof InvalidAgentNameError)
|
|
55394
|
-
return
|
|
55395
|
-
return
|
|
56695
|
+
return json5({ error: e.message, suggestions: e.suggestions }, 400);
|
|
56696
|
+
return json5({ error: e instanceof Error ? e.message : "Failed to get agent profile" }, 500);
|
|
55396
56697
|
}
|
|
55397
56698
|
}
|
|
55398
|
-
function handleAgentQueue(agentId, _ctx,
|
|
56699
|
+
function handleAgentQueue(agentId, _ctx, json5, taskToSummary2) {
|
|
55399
56700
|
const aliasSet = assignedToAliasSet(getDatabase(), agentId);
|
|
55400
56701
|
const pending = listTasks({ status: "pending" });
|
|
55401
56702
|
const queue = pending.filter((t) => aliasSet.has((t.assigned_to ?? "").toLowerCase()) || t.agent_id === agentId || !t.assigned_to && !t.locked_by);
|
|
55402
56703
|
const order = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
55403
56704
|
queue.sort((a, b) => (order[a.priority] ?? 4) - (order[b.priority] ?? 4) || new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
|
|
55404
|
-
return
|
|
56705
|
+
return json5(queue.map((t) => taskToSummary2(t)));
|
|
55405
56706
|
}
|
|
55406
|
-
async function handleClaimTask(req, _ctx,
|
|
56707
|
+
async function handleClaimTask(req, _ctx, json5, taskToSummary2) {
|
|
55407
56708
|
try {
|
|
55408
56709
|
const body2 = await req.json();
|
|
55409
56710
|
const agentId = body2.agent_id || "anonymous";
|
|
55410
56711
|
const task3 = claimNextTask(agentId, body2.project_id ? { project_id: body2.project_id } : undefined);
|
|
55411
|
-
return
|
|
56712
|
+
return json5({ task: task3 ? taskToSummary2(task3) : null });
|
|
55412
56713
|
} catch (e) {
|
|
55413
|
-
return
|
|
56714
|
+
return json5({ error: e instanceof Error ? e.message : "Failed to claim" }, 500);
|
|
55414
56715
|
}
|
|
55415
56716
|
}
|
|
55416
|
-
function handleListOrgs(_ctx,
|
|
55417
|
-
return
|
|
56717
|
+
function handleListOrgs(_ctx, json5) {
|
|
56718
|
+
return json5(listOrgs());
|
|
55418
56719
|
}
|
|
55419
|
-
async function handleCreateOrg(req, _ctx,
|
|
56720
|
+
async function handleCreateOrg(req, _ctx, json5) {
|
|
55420
56721
|
try {
|
|
55421
56722
|
const body2 = await req.json();
|
|
55422
56723
|
if (!body2.name)
|
|
55423
|
-
return
|
|
55424
|
-
return
|
|
56724
|
+
return json5({ error: "Missing name" }, 400);
|
|
56725
|
+
return json5(createOrg(body2), 201);
|
|
55425
56726
|
} catch (e) {
|
|
55426
|
-
return
|
|
56727
|
+
return json5({ error: e instanceof Error ? e.message : "Failed" }, 500);
|
|
55427
56728
|
}
|
|
55428
56729
|
}
|
|
55429
|
-
async function handleUpdateOrg(id, req, _ctx,
|
|
56730
|
+
async function handleUpdateOrg(id, req, _ctx, json5) {
|
|
55430
56731
|
try {
|
|
55431
56732
|
const body2 = await req.json();
|
|
55432
|
-
return
|
|
56733
|
+
return json5(updateOrg(id, body2));
|
|
55433
56734
|
} catch (e) {
|
|
55434
|
-
return
|
|
56735
|
+
return json5({ error: e instanceof Error ? e.message : "Failed" }, 500);
|
|
55435
56736
|
}
|
|
55436
56737
|
}
|
|
55437
|
-
function handleDeleteOrg(id, _ctx,
|
|
56738
|
+
function handleDeleteOrg(id, _ctx, json5) {
|
|
55438
56739
|
const deleted = deleteOrg(id);
|
|
55439
|
-
return
|
|
56740
|
+
return json5(deleted ? { success: true } : { error: "Not found" }, deleted ? 200 : 404);
|
|
55440
56741
|
}
|
|
55441
|
-
function handleOrgChart(_ctx,
|
|
55442
|
-
return
|
|
56742
|
+
function handleOrgChart(_ctx, json5) {
|
|
56743
|
+
return json5(getOrgChart());
|
|
55443
56744
|
}
|
|
55444
|
-
function handleAgentTeam(agentId, _ctx,
|
|
55445
|
-
return
|
|
56745
|
+
function handleAgentTeam(agentId, _ctx, json5) {
|
|
56746
|
+
return json5(getDirectReports(decodeURIComponent(agentId)));
|
|
55446
56747
|
}
|
|
55447
|
-
function handleListAgents(url, _ctx,
|
|
56748
|
+
function handleListAgents(url, _ctx, json5) {
|
|
55448
56749
|
const aFieldsParam = url.searchParams.get("fields");
|
|
55449
56750
|
const aFields = aFieldsParam ? aFieldsParam.split(",").map((f) => f.trim()).filter(Boolean) : undefined;
|
|
55450
56751
|
const agents = listAgents();
|
|
55451
|
-
return
|
|
56752
|
+
return json5(aFields ? agents.map((a) => Object.fromEntries(aFields.map((f) => [f, a[f] ?? null]))) : agents);
|
|
55452
56753
|
}
|
|
55453
|
-
async function handleRegisterAgent(req, _ctx,
|
|
56754
|
+
async function handleRegisterAgent(req, _ctx, json5) {
|
|
55454
56755
|
try {
|
|
55455
56756
|
const body2 = await req.json();
|
|
55456
56757
|
if (!body2.name)
|
|
55457
|
-
return
|
|
56758
|
+
return json5({ error: "Missing name" }, 400);
|
|
55458
56759
|
const result = registerAgent({ name: body2.name, description: body2.description, session_id: body2.session_id, working_dir: body2.working_dir });
|
|
55459
56760
|
if (isAgentConflict(result))
|
|
55460
|
-
return
|
|
55461
|
-
return
|
|
56761
|
+
return json5({ error: result.message, conflict: true }, 409);
|
|
56762
|
+
return json5(result, 201);
|
|
55462
56763
|
} catch (e) {
|
|
55463
56764
|
if (e instanceof InvalidAgentNameError)
|
|
55464
|
-
return
|
|
55465
|
-
return
|
|
56765
|
+
return json5({ error: e.message, suggestions: e.suggestions }, 400);
|
|
56766
|
+
return json5({ error: e instanceof Error ? e.message : "Failed to register agent" }, 500);
|
|
55466
56767
|
}
|
|
55467
56768
|
}
|
|
55468
|
-
async function handleUpdateAgent(id, req, _ctx,
|
|
56769
|
+
async function handleUpdateAgent(id, req, _ctx, json5) {
|
|
55469
56770
|
try {
|
|
55470
56771
|
const body2 = await req.json();
|
|
55471
56772
|
const agent = updateAgent(id, body2);
|
|
55472
|
-
return
|
|
56773
|
+
return json5(agent);
|
|
55473
56774
|
} catch (e) {
|
|
55474
56775
|
if (e instanceof InvalidAgentNameError)
|
|
55475
|
-
return
|
|
55476
|
-
return
|
|
56776
|
+
return json5({ error: e.message, suggestions: e.suggestions }, 400);
|
|
56777
|
+
return json5({ error: e instanceof Error ? e.message : "Failed to update agent" }, 500);
|
|
55477
56778
|
}
|
|
55478
56779
|
}
|
|
55479
|
-
function handleDeleteAgent(id, _ctx,
|
|
56780
|
+
function handleDeleteAgent(id, _ctx, json5) {
|
|
55480
56781
|
const deleted = deleteAgent(id);
|
|
55481
56782
|
if (!deleted)
|
|
55482
|
-
return
|
|
55483
|
-
return
|
|
56783
|
+
return json5({ error: "Agent not found" }, 404);
|
|
56784
|
+
return json5({ success: true });
|
|
55484
56785
|
}
|
|
55485
|
-
async function handleBulkDeleteAgents(req, _ctx,
|
|
56786
|
+
async function handleBulkDeleteAgents(req, _ctx, json5) {
|
|
55486
56787
|
try {
|
|
55487
56788
|
const body2 = await req.json();
|
|
55488
56789
|
if (!body2.ids?.length || body2.action !== "delete")
|
|
55489
|
-
return
|
|
56790
|
+
return json5({ error: "Missing ids or invalid action" }, 400);
|
|
55490
56791
|
let succeeded = 0;
|
|
55491
56792
|
for (const id of body2.ids) {
|
|
55492
56793
|
if (deleteAgent(id))
|
|
55493
56794
|
succeeded++;
|
|
55494
56795
|
}
|
|
55495
|
-
return
|
|
56796
|
+
return json5({ succeeded, failed: body2.ids.length - succeeded });
|
|
55496
56797
|
} catch (e) {
|
|
55497
|
-
return
|
|
56798
|
+
return json5({ error: e instanceof Error ? e.message : "Failed" }, 500);
|
|
55498
56799
|
}
|
|
55499
56800
|
}
|
|
55500
|
-
async function handleBulkDeleteProjects(req, _ctx,
|
|
56801
|
+
async function handleBulkDeleteProjects(req, _ctx, json5) {
|
|
55501
56802
|
try {
|
|
55502
56803
|
const body2 = await req.json();
|
|
55503
56804
|
if (!body2.ids?.length || body2.action !== "delete")
|
|
55504
|
-
return
|
|
56805
|
+
return json5({ error: "Missing ids or invalid action" }, 400);
|
|
55505
56806
|
let succeeded = 0;
|
|
55506
56807
|
for (const id of body2.ids) {
|
|
55507
56808
|
if (deleteProject(id))
|
|
55508
56809
|
succeeded++;
|
|
55509
56810
|
}
|
|
55510
|
-
return
|
|
56811
|
+
return json5({ succeeded, failed: body2.ids.length - succeeded });
|
|
55511
56812
|
} catch (e) {
|
|
55512
|
-
return
|
|
56813
|
+
return json5({ error: e instanceof Error ? e.message : "Failed" }, 500);
|
|
55513
56814
|
}
|
|
55514
56815
|
}
|
|
55515
|
-
function handleDoctor(_ctx,
|
|
56816
|
+
function handleDoctor(_ctx, json5) {
|
|
55516
56817
|
const { runTodosDoctor: runTodosDoctor2 } = (init_doctor(), __toCommonJS(exports_doctor));
|
|
55517
|
-
return
|
|
56818
|
+
return json5(runTodosDoctor2({ apply: false }));
|
|
55518
56819
|
}
|
|
55519
|
-
function handleReport(_req, url, _ctx,
|
|
56820
|
+
function handleReport(_req, url, _ctx, json5) {
|
|
55520
56821
|
const days = parseInt(url.searchParams.get("days") || "7", 10);
|
|
55521
56822
|
const projectId = url.searchParams.get("project_id") || undefined;
|
|
55522
56823
|
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString();
|
|
@@ -55532,62 +56833,62 @@ function handleReport(_req, url, _ctx, json4) {
|
|
|
55532
56833
|
byDay[day] = (byDay[day] || 0) + 1;
|
|
55533
56834
|
}
|
|
55534
56835
|
const completionRate = changed.length > 0 ? Math.round(completed.length / changed.length * 100) : 0;
|
|
55535
|
-
return
|
|
56836
|
+
return json5({ days, period_since: since, total: all.length, stats: stats2, changed: changed.length, completed: completed.length, failed: failed.length, completion_rate: completionRate, by_day: byDay });
|
|
55536
56837
|
}
|
|
55537
|
-
function handleActivity(_req, url, _ctx,
|
|
56838
|
+
function handleActivity(_req, url, _ctx, json5) {
|
|
55538
56839
|
const limit = parseInt(url.searchParams.get("limit") || "50", 10);
|
|
55539
|
-
return
|
|
56840
|
+
return json5(getRecentActivity(limit));
|
|
55540
56841
|
}
|
|
55541
|
-
function handleTaskHistory(id, _ctx,
|
|
56842
|
+
function handleTaskHistory(id, _ctx, json5, url) {
|
|
55542
56843
|
const history = getTaskHistory(id);
|
|
55543
56844
|
const format = url?.searchParams.get("format") || "compact";
|
|
55544
56845
|
const limit = parseBoundedLimit(url?.searchParams.get("limit") || null, 20, 500);
|
|
55545
|
-
return
|
|
56846
|
+
return json5(format === "full" ? history : history.slice(0, limit));
|
|
55546
56847
|
}
|
|
55547
|
-
function handleListWebhooks(_ctx,
|
|
55548
|
-
return
|
|
56848
|
+
function handleListWebhooks(_ctx, json5) {
|
|
56849
|
+
return json5(listWebhooks());
|
|
55549
56850
|
}
|
|
55550
|
-
async function handleCreateWebhook(req, _ctx,
|
|
56851
|
+
async function handleCreateWebhook(req, _ctx, json5) {
|
|
55551
56852
|
try {
|
|
55552
56853
|
const body2 = await req.json();
|
|
55553
56854
|
if (!body2.url)
|
|
55554
|
-
return
|
|
55555
|
-
return
|
|
56855
|
+
return json5({ error: "Missing url" }, 400);
|
|
56856
|
+
return json5(createWebhook(body2), 201);
|
|
55556
56857
|
} catch (e) {
|
|
55557
|
-
return
|
|
56858
|
+
return json5({ error: e instanceof Error ? e.message : "Failed" }, 500);
|
|
55558
56859
|
}
|
|
55559
56860
|
}
|
|
55560
|
-
function handleDeleteWebhook(id, _ctx,
|
|
56861
|
+
function handleDeleteWebhook(id, _ctx, json5) {
|
|
55561
56862
|
const deleted = deleteWebhook(id);
|
|
55562
|
-
return
|
|
56863
|
+
return json5(deleted ? { success: true } : { error: "Not found" }, deleted ? 200 : 404);
|
|
55563
56864
|
}
|
|
55564
|
-
function handleListTemplates(_ctx,
|
|
55565
|
-
return
|
|
56865
|
+
function handleListTemplates(_ctx, json5) {
|
|
56866
|
+
return json5(listTemplates());
|
|
55566
56867
|
}
|
|
55567
|
-
async function handleCreateTemplate(req, _ctx,
|
|
56868
|
+
async function handleCreateTemplate(req, _ctx, json5) {
|
|
55568
56869
|
try {
|
|
55569
56870
|
const body2 = await req.json();
|
|
55570
56871
|
if (!body2.name || !body2.title_pattern)
|
|
55571
|
-
return
|
|
55572
|
-
return
|
|
56872
|
+
return json5({ error: "Missing name or title_pattern" }, 400);
|
|
56873
|
+
return json5(createTemplate(body2), 201);
|
|
55573
56874
|
} catch (e) {
|
|
55574
|
-
return
|
|
56875
|
+
return json5({ error: e instanceof Error ? e.message : "Failed" }, 500);
|
|
55575
56876
|
}
|
|
55576
56877
|
}
|
|
55577
|
-
function handleDeleteTemplate(id, _ctx,
|
|
56878
|
+
function handleDeleteTemplate(id, _ctx, json5) {
|
|
55578
56879
|
const deleted = deleteTemplate(id);
|
|
55579
|
-
return
|
|
56880
|
+
return json5(deleted ? { success: true } : { error: "Not found" }, deleted ? 200 : 404);
|
|
55580
56881
|
}
|
|
55581
|
-
function handleListPlans(url, _ctx,
|
|
56882
|
+
function handleListPlans(url, _ctx, json5) {
|
|
55582
56883
|
const projectId = url.searchParams.get("project_id") || undefined;
|
|
55583
56884
|
const plans = listPlans(projectId);
|
|
55584
|
-
return
|
|
56885
|
+
return json5(plans);
|
|
55585
56886
|
}
|
|
55586
|
-
async function handleCreatePlan(req, _ctx,
|
|
56887
|
+
async function handleCreatePlan(req, _ctx, json5) {
|
|
55587
56888
|
try {
|
|
55588
56889
|
const body2 = await req.json();
|
|
55589
56890
|
if (!body2.name)
|
|
55590
|
-
return
|
|
56891
|
+
return json5({ error: "Missing 'name'" }, 400);
|
|
55591
56892
|
const plan = createPlan({
|
|
55592
56893
|
name: body2.name,
|
|
55593
56894
|
slug: body2.slug,
|
|
@@ -55597,49 +56898,49 @@ async function handleCreatePlan(req, _ctx, json4) {
|
|
|
55597
56898
|
agent_id: body2.agent_id,
|
|
55598
56899
|
status: body2.status
|
|
55599
56900
|
});
|
|
55600
|
-
return
|
|
56901
|
+
return json5(plan, 201);
|
|
55601
56902
|
} catch (e) {
|
|
55602
|
-
return
|
|
56903
|
+
return json5({ error: e instanceof Error ? e.message : "Failed to create plan" }, 500);
|
|
55603
56904
|
}
|
|
55604
56905
|
}
|
|
55605
|
-
async function handleBulkDeletePlans(req, _ctx,
|
|
56906
|
+
async function handleBulkDeletePlans(req, _ctx, json5) {
|
|
55606
56907
|
try {
|
|
55607
56908
|
const body2 = await req.json();
|
|
55608
56909
|
if (!body2.ids?.length || body2.action !== "delete")
|
|
55609
|
-
return
|
|
56910
|
+
return json5({ error: "Missing ids or invalid action" }, 400);
|
|
55610
56911
|
let succeeded = 0;
|
|
55611
56912
|
for (const id of body2.ids) {
|
|
55612
56913
|
if (deletePlan(id))
|
|
55613
56914
|
succeeded++;
|
|
55614
56915
|
}
|
|
55615
|
-
return
|
|
56916
|
+
return json5({ succeeded, failed: body2.ids.length - succeeded });
|
|
55616
56917
|
} catch (e) {
|
|
55617
|
-
return
|
|
56918
|
+
return json5({ error: e instanceof Error ? e.message : "Failed" }, 500);
|
|
55618
56919
|
}
|
|
55619
56920
|
}
|
|
55620
|
-
function handleGetPlan(id, _ctx,
|
|
56921
|
+
function handleGetPlan(id, _ctx, json5, taskToSummary2) {
|
|
55621
56922
|
const plan = getPlan(id);
|
|
55622
56923
|
if (!plan)
|
|
55623
|
-
return
|
|
56924
|
+
return json5({ error: "Plan not found" }, 404);
|
|
55624
56925
|
const tasks = listTasks({ plan_id: id });
|
|
55625
|
-
return
|
|
56926
|
+
return json5({ ...plan, tasks: tasks.map((t) => taskToSummary2(t)) });
|
|
55626
56927
|
}
|
|
55627
|
-
async function handleUpdatePlan(id, req, _ctx,
|
|
56928
|
+
async function handleUpdatePlan(id, req, _ctx, json5) {
|
|
55628
56929
|
try {
|
|
55629
56930
|
const body2 = await req.json();
|
|
55630
56931
|
const plan = updatePlan(id, body2);
|
|
55631
|
-
return
|
|
56932
|
+
return json5(plan);
|
|
55632
56933
|
} catch (e) {
|
|
55633
|
-
return
|
|
56934
|
+
return json5({ error: e instanceof Error ? e.message : "Failed to update plan" }, 500);
|
|
55634
56935
|
}
|
|
55635
56936
|
}
|
|
55636
|
-
function handleDeletePlan(id, _ctx,
|
|
56937
|
+
function handleDeletePlan(id, _ctx, json5) {
|
|
55637
56938
|
const deleted = deletePlan(id);
|
|
55638
56939
|
if (!deleted)
|
|
55639
|
-
return
|
|
55640
|
-
return
|
|
56940
|
+
return json5({ error: "Plan not found" }, 404);
|
|
56941
|
+
return json5({ success: true });
|
|
55641
56942
|
}
|
|
55642
|
-
function handleStaticFiles(path, method, ctx,
|
|
56943
|
+
function handleStaticFiles(path, method, ctx, json5, serveStaticFile2) {
|
|
55643
56944
|
if (!ctx.dashboardExists || method !== "GET" && method !== "HEAD")
|
|
55644
56945
|
return null;
|
|
55645
56946
|
if (path !== "/") {
|
|
@@ -55647,7 +56948,7 @@ function handleStaticFiles(path, method, ctx, json4, serveStaticFile2) {
|
|
|
55647
56948
|
const resolvedFile = resolve16(filePath);
|
|
55648
56949
|
const resolvedBase = resolve16(ctx.dashboardDir);
|
|
55649
56950
|
if (!resolvedFile.startsWith(resolvedBase + sep3) && resolvedFile !== resolvedBase) {
|
|
55650
|
-
return
|
|
56951
|
+
return json5({ error: "Forbidden" }, 403);
|
|
55651
56952
|
}
|
|
55652
56953
|
const res2 = serveStaticFile2(filePath);
|
|
55653
56954
|
if (res2)
|
|
@@ -55703,6 +57004,19 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
55703
57004
|
TaskManifestReceipt: taskManifestReceiptSchema,
|
|
55704
57005
|
TaskManifestApplyResult: taskManifestApplyResultSchema,
|
|
55705
57006
|
TaskManifestApplyResponse: taskManifestApplyResponseSchema,
|
|
57007
|
+
TaskSubtreeTransferExpectedTask: taskSubtreeTransferExpectedTaskSchema,
|
|
57008
|
+
TaskSubtreeTransferSharedPlanSplit: taskSubtreeTransferSharedPlanSplitSchema,
|
|
57009
|
+
TaskSubtreeTransferInspectRequest: taskSubtreeTransferInspectRequestSchema,
|
|
57010
|
+
TaskSubtreeTransferInspection: taskSubtreeTransferInspectionSchema,
|
|
57011
|
+
TaskSubtreeTransferApplyRequest: taskSubtreeTransferApplyRequestSchema,
|
|
57012
|
+
TaskSubtreeTransferTaskImage: taskSubtreeTransferTaskImageSchema,
|
|
57013
|
+
TaskSubtreeTransferPlanImage: taskSubtreeTransferPlanImageSchema,
|
|
57014
|
+
TaskSubtreeTransferImage: taskSubtreeTransferImageSchema,
|
|
57015
|
+
TaskSubtreeTransferReceipt: taskSubtreeTransferReceiptSchema,
|
|
57016
|
+
TaskSubtreeTransferResult: taskSubtreeTransferResultSchema,
|
|
57017
|
+
TaskSubtreeTransferCapability: taskSubtreeTransferCapabilitySchema,
|
|
57018
|
+
TaskSubtreeTransferRollbackRequest: taskSubtreeTransferRollbackRequestSchema,
|
|
57019
|
+
TaskSubtreeTransferReadExactRequest: taskSubtreeTransferReadExactRequestSchema,
|
|
55706
57020
|
TaskManifestCompensateRequest: taskManifestCompensateRequestSchema,
|
|
55707
57021
|
TaskManifestCompensationResult: taskManifestCompensationResultSchema,
|
|
55708
57022
|
TaskManifestCompensateResponse: taskManifestCompensateResponseSchema,
|
|
@@ -55907,7 +57221,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
55907
57221
|
slug: { type: "string", minLength: 1, pattern: ".*[A-Za-z0-9].*" },
|
|
55908
57222
|
name: { type: "string" },
|
|
55909
57223
|
description: { type: "string" },
|
|
55910
|
-
metadata: { type: "object", additionalProperties: true }
|
|
57224
|
+
metadata: { type: "object", additionalProperties: true },
|
|
57225
|
+
project_id: { type: "string", nullable: true }
|
|
55911
57226
|
}
|
|
55912
57227
|
},
|
|
55913
57228
|
CreateTaskCommentInput: {
|
|
@@ -57051,6 +58366,160 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
57051
58366
|
}
|
|
57052
58367
|
}
|
|
57053
58368
|
},
|
|
58369
|
+
"/v1/task-subtree-transfer/capability": {
|
|
58370
|
+
get: {
|
|
58371
|
+
operationId: "getTaskSubtreeTransferCapability",
|
|
58372
|
+
summary: "Read the current task-subtree-transfer authority capability and tenant",
|
|
58373
|
+
responses: {
|
|
58374
|
+
"200": {
|
|
58375
|
+
content: {
|
|
58376
|
+
"application/json": {
|
|
58377
|
+
schema: {
|
|
58378
|
+
type: "object",
|
|
58379
|
+
additionalProperties: false,
|
|
58380
|
+
required: ["capability"],
|
|
58381
|
+
properties: {
|
|
58382
|
+
capability: { $ref: "#/components/schemas/TaskSubtreeTransferCapability" }
|
|
58383
|
+
}
|
|
58384
|
+
}
|
|
58385
|
+
}
|
|
58386
|
+
}
|
|
58387
|
+
},
|
|
58388
|
+
"401": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
58389
|
+
"503": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
58390
|
+
}
|
|
58391
|
+
}
|
|
58392
|
+
},
|
|
58393
|
+
"/v1/task-subtree-transfer/inspect": {
|
|
58394
|
+
post: {
|
|
58395
|
+
operationId: "inspectTaskSubtreeTransfer",
|
|
58396
|
+
summary: "Inspect one exact task subtree and complete source population snapshot",
|
|
58397
|
+
requestBody: {
|
|
58398
|
+
required: true,
|
|
58399
|
+
content: {
|
|
58400
|
+
"application/json": {
|
|
58401
|
+
schema: { $ref: "#/components/schemas/TaskSubtreeTransferInspectRequest" }
|
|
58402
|
+
}
|
|
58403
|
+
}
|
|
58404
|
+
},
|
|
58405
|
+
responses: {
|
|
58406
|
+
"200": {
|
|
58407
|
+
content: {
|
|
58408
|
+
"application/json": {
|
|
58409
|
+
schema: {
|
|
58410
|
+
type: "object",
|
|
58411
|
+
additionalProperties: false,
|
|
58412
|
+
required: ["inspection"],
|
|
58413
|
+
properties: {
|
|
58414
|
+
inspection: { $ref: "#/components/schemas/TaskSubtreeTransferInspection" }
|
|
58415
|
+
}
|
|
58416
|
+
}
|
|
58417
|
+
}
|
|
58418
|
+
}
|
|
58419
|
+
},
|
|
58420
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
58421
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
58422
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
58423
|
+
}
|
|
58424
|
+
}
|
|
58425
|
+
},
|
|
58426
|
+
"/v1/task-subtree-transfer/apply": {
|
|
58427
|
+
post: {
|
|
58428
|
+
operationId: "applyTaskSubtreeTransfer",
|
|
58429
|
+
summary: "Apply one exact task subtree transfer atomically with CAS and immutable receipt",
|
|
58430
|
+
requestBody: {
|
|
58431
|
+
required: true,
|
|
58432
|
+
content: {
|
|
58433
|
+
"application/json": {
|
|
58434
|
+
schema: { $ref: "#/components/schemas/TaskSubtreeTransferApplyRequest" }
|
|
58435
|
+
}
|
|
58436
|
+
}
|
|
58437
|
+
},
|
|
58438
|
+
responses: {
|
|
58439
|
+
"201": {
|
|
58440
|
+
content: {
|
|
58441
|
+
"application/json": {
|
|
58442
|
+
schema: {
|
|
58443
|
+
type: "object",
|
|
58444
|
+
additionalProperties: false,
|
|
58445
|
+
required: ["result"],
|
|
58446
|
+
properties: {
|
|
58447
|
+
result: { $ref: "#/components/schemas/TaskSubtreeTransferResult" }
|
|
58448
|
+
}
|
|
58449
|
+
}
|
|
58450
|
+
}
|
|
58451
|
+
}
|
|
58452
|
+
},
|
|
58453
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
58454
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
58455
|
+
"503": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
58456
|
+
}
|
|
58457
|
+
}
|
|
58458
|
+
},
|
|
58459
|
+
"/v1/task-subtree-transfer/read-exact": {
|
|
58460
|
+
post: {
|
|
58461
|
+
operationId: "readExactTaskSubtreeTransfer",
|
|
58462
|
+
summary: "Read one exact immutable task-subtree-transfer receipt",
|
|
58463
|
+
requestBody: {
|
|
58464
|
+
required: true,
|
|
58465
|
+
content: {
|
|
58466
|
+
"application/json": {
|
|
58467
|
+
schema: { $ref: "#/components/schemas/TaskSubtreeTransferReadExactRequest" }
|
|
58468
|
+
}
|
|
58469
|
+
}
|
|
58470
|
+
},
|
|
58471
|
+
responses: {
|
|
58472
|
+
"200": {
|
|
58473
|
+
content: {
|
|
58474
|
+
"application/json": {
|
|
58475
|
+
schema: {
|
|
58476
|
+
type: "object",
|
|
58477
|
+
additionalProperties: false,
|
|
58478
|
+
required: ["result"],
|
|
58479
|
+
properties: {
|
|
58480
|
+
result: { $ref: "#/components/schemas/TaskSubtreeTransferResult" }
|
|
58481
|
+
}
|
|
58482
|
+
}
|
|
58483
|
+
}
|
|
58484
|
+
}
|
|
58485
|
+
},
|
|
58486
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
58487
|
+
}
|
|
58488
|
+
}
|
|
58489
|
+
},
|
|
58490
|
+
"/v1/task-subtree-transfer/rollback": {
|
|
58491
|
+
post: {
|
|
58492
|
+
operationId: "rollbackTaskSubtreeTransfer",
|
|
58493
|
+
summary: "Rollback one exact task-subtree-transfer receipt with CAS protection",
|
|
58494
|
+
requestBody: {
|
|
58495
|
+
required: true,
|
|
58496
|
+
content: {
|
|
58497
|
+
"application/json": {
|
|
58498
|
+
schema: { $ref: "#/components/schemas/TaskSubtreeTransferRollbackRequest" }
|
|
58499
|
+
}
|
|
58500
|
+
}
|
|
58501
|
+
},
|
|
58502
|
+
responses: {
|
|
58503
|
+
"201": {
|
|
58504
|
+
content: {
|
|
58505
|
+
"application/json": {
|
|
58506
|
+
schema: {
|
|
58507
|
+
type: "object",
|
|
58508
|
+
additionalProperties: false,
|
|
58509
|
+
required: ["result"],
|
|
58510
|
+
properties: {
|
|
58511
|
+
result: { $ref: "#/components/schemas/TaskSubtreeTransferResult" }
|
|
58512
|
+
}
|
|
58513
|
+
}
|
|
58514
|
+
}
|
|
58515
|
+
}
|
|
58516
|
+
},
|
|
58517
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
58518
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
58519
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
58520
|
+
}
|
|
58521
|
+
}
|
|
58522
|
+
},
|
|
57054
58523
|
"/v1/task-manifest/capability": {
|
|
57055
58524
|
get: {
|
|
57056
58525
|
operationId: "getTaskManifestCapability",
|
|
@@ -58004,7 +59473,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
58004
59473
|
}
|
|
58005
59474
|
});
|
|
58006
59475
|
}
|
|
58007
|
-
var taskSchema, taskManifestBoundsSchema, taskManifestCapabilitySchema, taskManifestCapabilityResponseSchema, projectSchema, taskManifestBindingLookupRequestSchema, taskManifestBindingLookupResultSchema, taskManifestSchema, taskManifestReceiptSchema, taskManifestApplyResultSchema, taskManifestApplyResponseSchema, taskManifestCompensateRequestSchema, taskManifestCompensationResultSchema, taskManifestCompensateResponseSchema, taskManifestReadExactRequestSchema, taskManifestBindingLookupResponseSchema, taskListSchema, projectTaskListEnsureReceiptSchema, projectTaskListEnsureResultSchema, projectTaskListRollbackResultSchema, taskCommentSchema, staleLockHandoffInputSchema, staleLockHandoffReceiptSchema, taskGitRefSchema, planSchema, planProjectLinkReceiptSchema, planProjectLinkResultSchema, planProjectLinkRollbackResultSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema, projectRegistrationBoundsProperties, projectRegistrationReceiptSchema, projectRegistrationCapabilitySchema, projectRegistrationRequestSchema, projectRegistrationLookupRequestSchema, priorRegistrationAdoptionValidationSchema, projectResourceSchema, projectResourcePageSchema;
|
|
59476
|
+
var taskSchema, taskManifestBoundsSchema, taskManifestCapabilitySchema, taskManifestCapabilityResponseSchema, projectSchema, taskManifestBindingLookupRequestSchema, taskManifestBindingLookupResultSchema, taskManifestSchema, taskManifestReceiptSchema, taskManifestApplyResultSchema, taskManifestApplyResponseSchema, taskSubtreeTransferExpectedTaskSchema, taskSubtreeTransferSharedPlanSplitSchema, taskSubtreeTransferInspectRequestSchema, taskSubtreeTransferInspectionSchema, taskSubtreeTransferApplyRequestSchema, taskSubtreeTransferTaskImageSchema, taskSubtreeTransferPlanImageSchema, taskSubtreeTransferImageSchema, taskSubtreeTransferReceiptSchema, taskSubtreeTransferResultSchema, taskSubtreeTransferCapabilitySchema, taskSubtreeTransferRollbackRequestSchema, taskSubtreeTransferReadExactRequestSchema, taskManifestCompensateRequestSchema, taskManifestCompensationResultSchema, taskManifestCompensateResponseSchema, taskManifestReadExactRequestSchema, taskManifestBindingLookupResponseSchema, taskListSchema, projectTaskListEnsureReceiptSchema, projectTaskListEnsureResultSchema, projectTaskListRollbackResultSchema, taskCommentSchema, staleLockHandoffInputSchema, staleLockHandoffReceiptSchema, taskGitRefSchema, planSchema, planProjectLinkReceiptSchema, planProjectLinkResultSchema, planProjectLinkRollbackResultSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema, projectRegistrationBoundsProperties, projectRegistrationReceiptSchema, projectRegistrationCapabilitySchema, projectRegistrationRequestSchema, projectRegistrationLookupRequestSchema, priorRegistrationAdoptionValidationSchema, projectResourceSchema, projectResourcePageSchema;
|
|
58008
59477
|
var init_openapi = __esm(() => {
|
|
58009
59478
|
init_package_version();
|
|
58010
59479
|
init_types();
|
|
@@ -58282,6 +59751,288 @@ var init_openapi = __esm(() => {
|
|
|
58282
59751
|
required: ["result"],
|
|
58283
59752
|
properties: { result: { $ref: "#/components/schemas/TaskManifestApplyResult" } }
|
|
58284
59753
|
};
|
|
59754
|
+
taskSubtreeTransferExpectedTaskSchema = {
|
|
59755
|
+
type: "object",
|
|
59756
|
+
additionalProperties: false,
|
|
59757
|
+
required: ["task_id", "version"],
|
|
59758
|
+
properties: {
|
|
59759
|
+
task_id: { type: "string", format: "uuid" },
|
|
59760
|
+
version: { type: "integer", minimum: 1 }
|
|
59761
|
+
}
|
|
59762
|
+
};
|
|
59763
|
+
taskSubtreeTransferSharedPlanSplitSchema = {
|
|
59764
|
+
type: "object",
|
|
59765
|
+
additionalProperties: false,
|
|
59766
|
+
required: ["source_plan_id", "destination_plan_id"],
|
|
59767
|
+
properties: {
|
|
59768
|
+
source_plan_id: { type: "string", format: "uuid" },
|
|
59769
|
+
destination_plan_id: { type: "string", format: "uuid" }
|
|
59770
|
+
}
|
|
59771
|
+
};
|
|
59772
|
+
taskSubtreeTransferInspectRequestSchema = {
|
|
59773
|
+
type: "object",
|
|
59774
|
+
additionalProperties: false,
|
|
59775
|
+
required: [
|
|
59776
|
+
"source_project_id",
|
|
59777
|
+
"destination_project_id",
|
|
59778
|
+
"destination_task_list_id",
|
|
59779
|
+
"root_task_id",
|
|
59780
|
+
"destination_parent_id"
|
|
59781
|
+
],
|
|
59782
|
+
properties: {
|
|
59783
|
+
source_project_id: { type: "string", format: "uuid" },
|
|
59784
|
+
destination_project_id: { type: "string", format: "uuid" },
|
|
59785
|
+
destination_task_list_id: { type: "string", format: "uuid" },
|
|
59786
|
+
root_task_id: { type: "string", format: "uuid" },
|
|
59787
|
+
destination_parent_id: { type: "string", format: "uuid", nullable: true }
|
|
59788
|
+
}
|
|
59789
|
+
};
|
|
59790
|
+
taskSubtreeTransferInspectionSchema = {
|
|
59791
|
+
type: "object",
|
|
59792
|
+
additionalProperties: false,
|
|
59793
|
+
required: [
|
|
59794
|
+
"source_project_id",
|
|
59795
|
+
"destination_project_id",
|
|
59796
|
+
"destination_task_list_id",
|
|
59797
|
+
"root_task_id",
|
|
59798
|
+
"destination_parent_id",
|
|
59799
|
+
"expected_root_parent_id",
|
|
59800
|
+
"source_population_digest",
|
|
59801
|
+
"expected_tasks",
|
|
59802
|
+
"contained_plan_ids",
|
|
59803
|
+
"shared_plan_ids",
|
|
59804
|
+
"complete"
|
|
59805
|
+
],
|
|
59806
|
+
properties: {
|
|
59807
|
+
...taskSubtreeTransferInspectRequestSchema.properties,
|
|
59808
|
+
expected_root_parent_id: { type: "string", format: "uuid", nullable: true },
|
|
59809
|
+
source_population_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
|
|
59810
|
+
expected_tasks: {
|
|
59811
|
+
type: "array",
|
|
59812
|
+
minItems: 1,
|
|
59813
|
+
items: { $ref: "#/components/schemas/TaskSubtreeTransferExpectedTask" }
|
|
59814
|
+
},
|
|
59815
|
+
contained_plan_ids: {
|
|
59816
|
+
type: "array",
|
|
59817
|
+
items: { type: "string", format: "uuid" }
|
|
59818
|
+
},
|
|
59819
|
+
shared_plan_ids: {
|
|
59820
|
+
type: "array",
|
|
59821
|
+
items: { type: "string", format: "uuid" }
|
|
59822
|
+
},
|
|
59823
|
+
complete: { type: "boolean", enum: [true] }
|
|
59824
|
+
}
|
|
59825
|
+
};
|
|
59826
|
+
taskSubtreeTransferApplyRequestSchema = {
|
|
59827
|
+
type: "object",
|
|
59828
|
+
additionalProperties: false,
|
|
59829
|
+
required: [
|
|
59830
|
+
"version",
|
|
59831
|
+
"operation_id",
|
|
59832
|
+
"step_id",
|
|
59833
|
+
"idempotency_key",
|
|
59834
|
+
"precondition_digest",
|
|
59835
|
+
"source_project_id",
|
|
59836
|
+
"destination_project_id",
|
|
59837
|
+
"destination_task_list_id",
|
|
59838
|
+
"root_task_id",
|
|
59839
|
+
"destination_parent_id",
|
|
59840
|
+
"expected_root_parent_id",
|
|
59841
|
+
"source_population_digest",
|
|
59842
|
+
"expected_tasks",
|
|
59843
|
+
"shared_plan_splits"
|
|
59844
|
+
],
|
|
59845
|
+
properties: {
|
|
59846
|
+
...taskSubtreeTransferInspectRequestSchema.properties,
|
|
59847
|
+
version: { type: "integer", enum: [1] },
|
|
59848
|
+
operation_id: { type: "string", minLength: 1, maxLength: 200 },
|
|
59849
|
+
step_id: { type: "string", minLength: 1, maxLength: 200 },
|
|
59850
|
+
idempotency_key: { type: "string", minLength: 1, maxLength: 240 },
|
|
59851
|
+
precondition_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
|
|
59852
|
+
expected_root_parent_id: { type: "string", format: "uuid", nullable: true },
|
|
59853
|
+
source_population_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
|
|
59854
|
+
expected_tasks: {
|
|
59855
|
+
type: "array",
|
|
59856
|
+
minItems: 1,
|
|
59857
|
+
items: { $ref: "#/components/schemas/TaskSubtreeTransferExpectedTask" }
|
|
59858
|
+
},
|
|
59859
|
+
shared_plan_splits: {
|
|
59860
|
+
type: "array",
|
|
59861
|
+
items: { $ref: "#/components/schemas/TaskSubtreeTransferSharedPlanSplit" }
|
|
59862
|
+
}
|
|
59863
|
+
}
|
|
59864
|
+
};
|
|
59865
|
+
taskSubtreeTransferTaskImageSchema = {
|
|
59866
|
+
type: "object",
|
|
59867
|
+
additionalProperties: false,
|
|
59868
|
+
required: [
|
|
59869
|
+
"task_id",
|
|
59870
|
+
"project_id",
|
|
59871
|
+
"parent_id",
|
|
59872
|
+
"plan_id",
|
|
59873
|
+
"task_list_id",
|
|
59874
|
+
"version",
|
|
59875
|
+
"updated_at"
|
|
59876
|
+
],
|
|
59877
|
+
properties: {
|
|
59878
|
+
task_id: { type: "string", format: "uuid" },
|
|
59879
|
+
project_id: { type: "string", format: "uuid", nullable: true },
|
|
59880
|
+
parent_id: { type: "string", format: "uuid", nullable: true },
|
|
59881
|
+
plan_id: { type: "string", format: "uuid", nullable: true },
|
|
59882
|
+
task_list_id: { type: "string", format: "uuid", nullable: true },
|
|
59883
|
+
version: { type: "integer", minimum: 1 },
|
|
59884
|
+
updated_at: { type: "string", format: "date-time" }
|
|
59885
|
+
}
|
|
59886
|
+
};
|
|
59887
|
+
taskSubtreeTransferPlanImageSchema = {
|
|
59888
|
+
type: "object",
|
|
59889
|
+
additionalProperties: false,
|
|
59890
|
+
required: ["plan_id", "project_id", "task_list_id", "updated_at"],
|
|
59891
|
+
properties: {
|
|
59892
|
+
plan_id: { type: "string", format: "uuid" },
|
|
59893
|
+
project_id: { type: "string", format: "uuid", nullable: true },
|
|
59894
|
+
task_list_id: { type: "string", format: "uuid", nullable: true },
|
|
59895
|
+
updated_at: { type: "string", format: "date-time" }
|
|
59896
|
+
}
|
|
59897
|
+
};
|
|
59898
|
+
taskSubtreeTransferImageSchema = {
|
|
59899
|
+
type: "object",
|
|
59900
|
+
additionalProperties: false,
|
|
59901
|
+
required: ["tasks", "plans"],
|
|
59902
|
+
properties: {
|
|
59903
|
+
tasks: {
|
|
59904
|
+
type: "array",
|
|
59905
|
+
items: { $ref: "#/components/schemas/TaskSubtreeTransferTaskImage" }
|
|
59906
|
+
},
|
|
59907
|
+
plans: {
|
|
59908
|
+
type: "array",
|
|
59909
|
+
items: { $ref: "#/components/schemas/TaskSubtreeTransferPlanImage" }
|
|
59910
|
+
}
|
|
59911
|
+
}
|
|
59912
|
+
};
|
|
59913
|
+
taskSubtreeTransferReceiptSchema = {
|
|
59914
|
+
type: "object",
|
|
59915
|
+
additionalProperties: false,
|
|
59916
|
+
required: [
|
|
59917
|
+
"receipt_id",
|
|
59918
|
+
"authority",
|
|
59919
|
+
"route",
|
|
59920
|
+
"schema_version",
|
|
59921
|
+
"kind",
|
|
59922
|
+
"operation_id",
|
|
59923
|
+
"step_id",
|
|
59924
|
+
"idempotency_key",
|
|
59925
|
+
"request_digest",
|
|
59926
|
+
"precondition_digest",
|
|
59927
|
+
"result_digest",
|
|
59928
|
+
"apply_receipt_id",
|
|
59929
|
+
"source_project_id",
|
|
59930
|
+
"destination_project_id",
|
|
59931
|
+
"destination_task_list_id",
|
|
59932
|
+
"root_task_id",
|
|
59933
|
+
"source_population_digest",
|
|
59934
|
+
"prior_image",
|
|
59935
|
+
"post_image",
|
|
59936
|
+
"shared_plan_splits",
|
|
59937
|
+
"created_at"
|
|
59938
|
+
],
|
|
59939
|
+
properties: {
|
|
59940
|
+
receipt_id: { type: "string", format: "uuid" },
|
|
59941
|
+
authority: { type: "string", enum: ["todos"] },
|
|
59942
|
+
route: { type: "string", enum: ["todos.task-subtree-transfer.v1"] },
|
|
59943
|
+
schema_version: { type: "integer", enum: [1] },
|
|
59944
|
+
kind: { type: "string", enum: ["apply", "rollback"] },
|
|
59945
|
+
operation_id: { type: "string" },
|
|
59946
|
+
step_id: { type: "string" },
|
|
59947
|
+
idempotency_key: { type: "string" },
|
|
59948
|
+
request_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
|
|
59949
|
+
precondition_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
|
|
59950
|
+
result_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
|
|
59951
|
+
apply_receipt_id: { type: "string", format: "uuid", nullable: true },
|
|
59952
|
+
source_project_id: { type: "string", format: "uuid" },
|
|
59953
|
+
destination_project_id: { type: "string", format: "uuid" },
|
|
59954
|
+
destination_task_list_id: { type: "string", format: "uuid" },
|
|
59955
|
+
root_task_id: { type: "string", format: "uuid" },
|
|
59956
|
+
source_population_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
|
|
59957
|
+
prior_image: { $ref: "#/components/schemas/TaskSubtreeTransferImage" },
|
|
59958
|
+
post_image: { $ref: "#/components/schemas/TaskSubtreeTransferImage" },
|
|
59959
|
+
shared_plan_splits: {
|
|
59960
|
+
type: "array",
|
|
59961
|
+
items: { $ref: "#/components/schemas/TaskSubtreeTransferSharedPlanSplit" }
|
|
59962
|
+
},
|
|
59963
|
+
created_at: { type: "string", format: "date-time" }
|
|
59964
|
+
}
|
|
59965
|
+
};
|
|
59966
|
+
taskSubtreeTransferResultSchema = {
|
|
59967
|
+
type: "object",
|
|
59968
|
+
additionalProperties: false,
|
|
59969
|
+
required: ["duplicate", "receipt", "moved_task_ids", "moved_plan_ids", "complete"],
|
|
59970
|
+
properties: {
|
|
59971
|
+
duplicate: { type: "boolean" },
|
|
59972
|
+
receipt: { $ref: "#/components/schemas/TaskSubtreeTransferReceipt" },
|
|
59973
|
+
moved_task_ids: { type: "array", items: { type: "string", format: "uuid" } },
|
|
59974
|
+
moved_plan_ids: { type: "array", items: { type: "string", format: "uuid" } },
|
|
59975
|
+
complete: { type: "boolean", enum: [true] }
|
|
59976
|
+
}
|
|
59977
|
+
};
|
|
59978
|
+
taskSubtreeTransferCapabilitySchema = {
|
|
59979
|
+
type: "object",
|
|
59980
|
+
additionalProperties: false,
|
|
59981
|
+
required: [
|
|
59982
|
+
"authority",
|
|
59983
|
+
"route",
|
|
59984
|
+
"schema_version",
|
|
59985
|
+
"tenant_id",
|
|
59986
|
+
"backend",
|
|
59987
|
+
"exact_descendant_closure",
|
|
59988
|
+
"complete_source_population_digest",
|
|
59989
|
+
"per_task_version_cas",
|
|
59990
|
+
"explicit_shared_plan_splits",
|
|
59991
|
+
"atomic_apply",
|
|
59992
|
+
"immutable_forward_inverse_receipts",
|
|
59993
|
+
"prior_image_receipts",
|
|
59994
|
+
"cas_protected_rollback",
|
|
59995
|
+
"preserves_descendant_parent_ids",
|
|
59996
|
+
"preserves_task_and_relation_identities"
|
|
59997
|
+
],
|
|
59998
|
+
properties: {
|
|
59999
|
+
authority: { type: "string", enum: ["todos"] },
|
|
60000
|
+
route: { type: "string", enum: ["todos.task-subtree-transfer.v1"] },
|
|
60001
|
+
schema_version: { type: "integer", enum: [1] },
|
|
60002
|
+
tenant_id: { type: "string", minLength: 1, maxLength: 200 },
|
|
60003
|
+
backend: { type: "string", enum: ["sqlite", "postgresql", "http"] },
|
|
60004
|
+
exact_descendant_closure: { type: "boolean", enum: [true] },
|
|
60005
|
+
complete_source_population_digest: { type: "boolean", enum: [true] },
|
|
60006
|
+
per_task_version_cas: { type: "boolean", enum: [true] },
|
|
60007
|
+
explicit_shared_plan_splits: { type: "boolean", enum: [true] },
|
|
60008
|
+
atomic_apply: { type: "boolean", enum: [true] },
|
|
60009
|
+
immutable_forward_inverse_receipts: { type: "boolean", enum: [true] },
|
|
60010
|
+
prior_image_receipts: { type: "boolean", enum: [true] },
|
|
60011
|
+
cas_protected_rollback: { type: "boolean", enum: [true] },
|
|
60012
|
+
preserves_descendant_parent_ids: { type: "boolean", enum: [true] },
|
|
60013
|
+
preserves_task_and_relation_identities: { type: "boolean", enum: [true] }
|
|
60014
|
+
}
|
|
60015
|
+
};
|
|
60016
|
+
taskSubtreeTransferRollbackRequestSchema = {
|
|
60017
|
+
type: "object",
|
|
60018
|
+
additionalProperties: false,
|
|
60019
|
+
required: ["receipt_id", "operation_id", "step_id", "idempotency_key", "precondition_digest"],
|
|
60020
|
+
properties: {
|
|
60021
|
+
receipt_id: { type: "string", format: "uuid" },
|
|
60022
|
+
operation_id: { type: "string", minLength: 1, maxLength: 200 },
|
|
60023
|
+
step_id: { type: "string", minLength: 1, maxLength: 200 },
|
|
60024
|
+
idempotency_key: { type: "string", minLength: 1, maxLength: 240 },
|
|
60025
|
+
precondition_digest: { type: "string", pattern: "^[0-9a-f]{64}$" }
|
|
60026
|
+
}
|
|
60027
|
+
};
|
|
60028
|
+
taskSubtreeTransferReadExactRequestSchema = {
|
|
60029
|
+
type: "object",
|
|
60030
|
+
additionalProperties: false,
|
|
60031
|
+
required: ["receipt_id"],
|
|
60032
|
+
properties: {
|
|
60033
|
+
receipt_id: { type: "string", format: "uuid" }
|
|
60034
|
+
}
|
|
60035
|
+
};
|
|
58285
60036
|
taskManifestCompensateRequestSchema = {
|
|
58286
60037
|
type: "object",
|
|
58287
60038
|
additionalProperties: false,
|
|
@@ -58960,8 +60711,8 @@ var exports_pr_groups = {};
|
|
|
58960
60711
|
__export(exports_pr_groups, {
|
|
58961
60712
|
handlePrGroupHttpRequest: () => handlePrGroupHttpRequest
|
|
58962
60713
|
});
|
|
58963
|
-
function
|
|
58964
|
-
return new Response(JSON.stringify(body2), { status:
|
|
60714
|
+
function json5(body2, status3 = 200) {
|
|
60715
|
+
return new Response(JSON.stringify(body2), { status: status3, headers: JSON_HEADERS4 });
|
|
58965
60716
|
}
|
|
58966
60717
|
function errorStatus2(error) {
|
|
58967
60718
|
switch (error.code) {
|
|
@@ -58995,26 +60746,26 @@ async function handlePrGroupHttpRequest(req, url, ledger, basePath, principal) {
|
|
|
58995
60746
|
const method = req.method.toUpperCase();
|
|
58996
60747
|
try {
|
|
58997
60748
|
if (!groupId && method === "POST" && action === undefined) {
|
|
58998
|
-
return
|
|
60749
|
+
return json5({ error: "unknown PR-group route", code: "PR_GROUP_NOT_FOUND" }, 404);
|
|
58999
60750
|
}
|
|
59000
60751
|
if (groupId === "admit" && !action) {
|
|
59001
60752
|
if (method !== "POST")
|
|
59002
|
-
return
|
|
60753
|
+
return json5({ error: "method not allowed" }, 405);
|
|
59003
60754
|
const body2 = await readJson2(req);
|
|
59004
60755
|
if (!body2)
|
|
59005
|
-
return
|
|
59006
|
-
return
|
|
60756
|
+
return json5({ error: "invalid JSON body", code: "PR_GROUP_INVALID_INPUT" }, 400);
|
|
60757
|
+
return json5(await ledger.admit(body2), 201);
|
|
59007
60758
|
}
|
|
59008
60759
|
if (!groupId)
|
|
59009
|
-
return
|
|
60760
|
+
return json5({ error: "PR group id is required", code: "PR_GROUP_INVALID_INPUT" }, 400);
|
|
59010
60761
|
if (!action && method === "GET") {
|
|
59011
|
-
return
|
|
60762
|
+
return json5({ view: await ledger.get(groupId) });
|
|
59012
60763
|
}
|
|
59013
60764
|
if (action === "events") {
|
|
59014
60765
|
if (method === "GET") {
|
|
59015
60766
|
const limit = url.searchParams.has("limit") ? Number(url.searchParams.get("limit")) : undefined;
|
|
59016
60767
|
const afterSequence = url.searchParams.has("after_sequence") ? Number(url.searchParams.get("after_sequence")) : undefined;
|
|
59017
|
-
return
|
|
60768
|
+
return json5({
|
|
59018
60769
|
history: await ledger.events(groupId, {
|
|
59019
60770
|
...limit !== undefined ? { limit } : {},
|
|
59020
60771
|
...afterSequence !== undefined ? { after_sequence: afterSequence } : {}
|
|
@@ -59024,72 +60775,49 @@ async function handlePrGroupHttpRequest(req, url, ledger, basePath, principal) {
|
|
|
59024
60775
|
if (method === "POST") {
|
|
59025
60776
|
const body2 = await readJson2(req);
|
|
59026
60777
|
if (!body2)
|
|
59027
|
-
return
|
|
59028
|
-
return
|
|
60778
|
+
return json5({ error: "invalid JSON body", code: "PR_GROUP_INVALID_INPUT" }, 400);
|
|
60779
|
+
return json5(await ledger.append({
|
|
59029
60780
|
...body2,
|
|
59030
60781
|
group_id: groupId,
|
|
59031
60782
|
authenticated_actor_id: principal?.actor_id ?? undefined,
|
|
59032
60783
|
authenticated_actor_run_id: principal?.actor_run_id ?? undefined
|
|
59033
60784
|
}), 201);
|
|
59034
60785
|
}
|
|
59035
|
-
return
|
|
60786
|
+
return json5({ error: "method not allowed" }, 405);
|
|
59036
60787
|
}
|
|
59037
60788
|
if (action === "recover") {
|
|
59038
60789
|
if (method !== "POST")
|
|
59039
|
-
return
|
|
60790
|
+
return json5({ error: "method not allowed" }, 405);
|
|
59040
60791
|
const body2 = await readJson2(req);
|
|
59041
60792
|
if (!body2)
|
|
59042
|
-
return
|
|
59043
|
-
return
|
|
60793
|
+
return json5({ error: "invalid JSON body", code: "PR_GROUP_INVALID_INPUT" }, 400);
|
|
60794
|
+
return json5(await ledger.recover({
|
|
59044
60795
|
...body2,
|
|
59045
60796
|
group_id: groupId
|
|
59046
60797
|
}), 201);
|
|
59047
60798
|
}
|
|
59048
|
-
return
|
|
60799
|
+
return json5({ error: "unknown PR-group route", code: "PR_GROUP_NOT_FOUND" }, 404);
|
|
59049
60800
|
} catch (cause) {
|
|
59050
60801
|
if (cause instanceof PrGroupLedgerError) {
|
|
59051
|
-
return
|
|
60802
|
+
return json5({
|
|
59052
60803
|
error: cause.message,
|
|
59053
60804
|
code: cause.code,
|
|
59054
60805
|
details: cause.details,
|
|
59055
60806
|
authoritative: true
|
|
59056
60807
|
}, errorStatus2(cause));
|
|
59057
60808
|
}
|
|
59058
|
-
return
|
|
60809
|
+
return json5({
|
|
59059
60810
|
error: cause instanceof Error ? cause.message : "internal PR-group error",
|
|
59060
60811
|
code: "PR_GROUP_ATOMICITY_UNAVAILABLE"
|
|
59061
60812
|
}, 500);
|
|
59062
60813
|
}
|
|
59063
60814
|
}
|
|
59064
|
-
var
|
|
60815
|
+
var JSON_HEADERS4;
|
|
59065
60816
|
var init_pr_groups = __esm(() => {
|
|
59066
60817
|
init_types3();
|
|
59067
|
-
|
|
60818
|
+
JSON_HEADERS4 = { "Content-Type": "application/json" };
|
|
59068
60819
|
});
|
|
59069
60820
|
|
|
59070
|
-
// src/lib/comment-cursor.ts
|
|
59071
|
-
function encodeCommentCursor(comment2) {
|
|
59072
|
-
return Buffer.from(JSON.stringify({ created_at: comment2.created_at, id: comment2.id }), "utf8").toString("base64url");
|
|
59073
|
-
}
|
|
59074
|
-
function decodeCommentCursor(value) {
|
|
59075
|
-
if (value.length > MAX_COMMENT_CURSOR_LENGTH)
|
|
59076
|
-
throw new Error("invalid comment cursor");
|
|
59077
|
-
let parsed;
|
|
59078
|
-
try {
|
|
59079
|
-
parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
|
|
59080
|
-
} catch {
|
|
59081
|
-
throw new Error("invalid comment cursor");
|
|
59082
|
-
}
|
|
59083
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
59084
|
-
throw new Error("invalid comment cursor");
|
|
59085
|
-
const cursor = parsed;
|
|
59086
|
-
if (typeof cursor["created_at"] !== "string" || cursor["created_at"].length > 64 || !Number.isFinite(Date.parse(cursor["created_at"])) || typeof cursor["id"] !== "string" || !cursor["id"] || cursor["id"].length > 256) {
|
|
59087
|
-
throw new Error("invalid comment cursor");
|
|
59088
|
-
}
|
|
59089
|
-
return { created_at: cursor["created_at"], id: cursor["id"] };
|
|
59090
|
-
}
|
|
59091
|
-
var MAX_COMMENT_CURSOR_LENGTH = 1024;
|
|
59092
|
-
|
|
59093
60821
|
// src/lib/project-task-list-ensure.ts
|
|
59094
60822
|
import { createHash as createHash17 } from "crypto";
|
|
59095
60823
|
function canonicalJson2(value) {
|
|
@@ -59099,11 +60827,11 @@ function canonicalJson2(value) {
|
|
|
59099
60827
|
return `[${value.map(canonicalJson2).join(",")}]`;
|
|
59100
60828
|
return `{${Object.entries(value).filter(([, item]) => item !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key2, item]) => `${JSON.stringify(key2)}:${canonicalJson2(item)}`).join(",")}}`;
|
|
59101
60829
|
}
|
|
59102
|
-
function
|
|
60830
|
+
function digest3(value) {
|
|
59103
60831
|
return createHash17("sha256").update(canonicalJson2(value)).digest("hex");
|
|
59104
60832
|
}
|
|
59105
60833
|
function deriveIdempotencyKey(projectId, slug) {
|
|
59106
|
-
return `ptlk_${
|
|
60834
|
+
return `ptlk_${digest3({ project_id: projectId, slug }).slice(0, 48)}`;
|
|
59107
60835
|
}
|
|
59108
60836
|
function normalizeIdempotencyKey(value, projectId, slug) {
|
|
59109
60837
|
const key2 = value?.trim() || deriveIdempotencyKey(projectId, slug);
|
|
@@ -59113,12 +60841,12 @@ function normalizeIdempotencyKey(value, projectId, slug) {
|
|
|
59113
60841
|
return key2;
|
|
59114
60842
|
}
|
|
59115
60843
|
function receiptId2(projectId, slug, idempotencyKey2) {
|
|
59116
|
-
return `ptlr_${
|
|
60844
|
+
return `ptlr_${digest3({ project_id: projectId, slug, idempotency_key: idempotencyKey2 }).slice(0, 48)}`;
|
|
59117
60845
|
}
|
|
59118
60846
|
function semanticListDigest(list) {
|
|
59119
60847
|
const metadata = { ...list.metadata ?? {} };
|
|
59120
60848
|
delete metadata[RECEIPT_METADATA_KEY];
|
|
59121
|
-
return
|
|
60849
|
+
return digest3({
|
|
59122
60850
|
project_id: list.project_id,
|
|
59123
60851
|
slug: list.slug,
|
|
59124
60852
|
name: list.name,
|
|
@@ -59147,7 +60875,7 @@ function receiptFor(store, project, list, idempotencyKey2) {
|
|
|
59147
60875
|
}
|
|
59148
60876
|
return {
|
|
59149
60877
|
schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
|
|
59150
|
-
receipt_id: owned ? marker.receipt_id : `ptlr_existing_${
|
|
60878
|
+
receipt_id: owned ? marker.receipt_id : `ptlr_existing_${digest3({ project_id: project.id, task_list_id: list.id }).slice(0, 39)}`,
|
|
59151
60879
|
idempotency_key: owned ? marker.idempotency_key : idempotencyKey2,
|
|
59152
60880
|
project_id: project.id,
|
|
59153
60881
|
task_list_id: list.id,
|
|
@@ -59326,7 +61054,7 @@ async function rollbackProjectTaskListEnsure(store, projectId, options) {
|
|
|
59326
61054
|
project_id: project.id,
|
|
59327
61055
|
task_list_id: list.id,
|
|
59328
61056
|
accepted_receipt_id: options.receipt_id,
|
|
59329
|
-
rollback_receipt_id: `ptlr_inverse_${
|
|
61057
|
+
rollback_receipt_id: `ptlr_inverse_${digest3({ accepted_receipt_id: options.receipt_id }).slice(0, 38)}`,
|
|
59330
61058
|
removed_at: new Date().toISOString()
|
|
59331
61059
|
};
|
|
59332
61060
|
}
|
|
@@ -59421,11 +61149,11 @@ __export(exports_v1, {
|
|
|
59421
61149
|
handleV1Request: () => handleV1Request,
|
|
59422
61150
|
countSnapshotRecords: () => countSnapshotRecords
|
|
59423
61151
|
});
|
|
59424
|
-
function
|
|
59425
|
-
return new Response(JSON.stringify(body2), { status:
|
|
61152
|
+
function json6(body2, status3 = 200) {
|
|
61153
|
+
return new Response(JSON.stringify(body2), { status: status3, headers: JSON_HEADERS5 });
|
|
59426
61154
|
}
|
|
59427
|
-
function error(
|
|
59428
|
-
return
|
|
61155
|
+
function error(status3, message, extra) {
|
|
61156
|
+
return json6({ error: message, ...extra ?? {} }, status3);
|
|
59429
61157
|
}
|
|
59430
61158
|
function enumQueryParam(url, name, vocabulary) {
|
|
59431
61159
|
const raw = url.searchParams.get(name);
|
|
@@ -59764,6 +61492,9 @@ function contextFromPrincipal(principal, body2) {
|
|
|
59764
61492
|
function redactComment3(comment2) {
|
|
59765
61493
|
return { ...comment2, content: redactEvidenceText(comment2.content) };
|
|
59766
61494
|
}
|
|
61495
|
+
function redactPlanComment2(comment2) {
|
|
61496
|
+
return { ...comment2, content: redactEvidenceText(comment2.content) };
|
|
61497
|
+
}
|
|
59767
61498
|
function normalizeImportSnapshot(raw) {
|
|
59768
61499
|
const body2 = raw && typeof raw === "object" ? raw : {};
|
|
59769
61500
|
const arr = (v) => Array.isArray(v) ? v : [];
|
|
@@ -59872,6 +61603,9 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
59872
61603
|
if (path === "/v1/task-manifest" || path.startsWith("/v1/task-manifest/")) {
|
|
59873
61604
|
return handleTodosTaskManifestHttpRequest(req, url, (dependencies.getTaskManifestAuthority ?? getCloudTaskManifestAuthority)());
|
|
59874
61605
|
}
|
|
61606
|
+
if (path === "/v1/task-subtree-transfer" || path.startsWith("/v1/task-subtree-transfer/")) {
|
|
61607
|
+
return handleTodosTaskSubtreeTransferHttpRequest(req, url, (dependencies.getTaskSubtreeTransferAuthority ?? getCloudTaskSubtreeTransferAuthority)());
|
|
61608
|
+
}
|
|
59875
61609
|
const store = (dependencies.getStorageAdapter ?? getCloudStorageAdapter)();
|
|
59876
61610
|
const segments = path.split("/").filter(Boolean);
|
|
59877
61611
|
const resource = segments[1];
|
|
@@ -59893,7 +61627,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
59893
61627
|
const presentSet = new Set(found.map((t) => t.id));
|
|
59894
61628
|
const present = ids2.filter((i) => presentSet.has(i));
|
|
59895
61629
|
const missing = ids2.filter((i) => !presentSet.has(i));
|
|
59896
|
-
return
|
|
61630
|
+
return json6({
|
|
59897
61631
|
requested: ids2.length,
|
|
59898
61632
|
present_count: present.length,
|
|
59899
61633
|
missing_count: missing.length,
|
|
@@ -59943,11 +61677,11 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
59943
61677
|
}
|
|
59944
61678
|
if (!existing) {
|
|
59945
61679
|
const task3 = await store.tasks.create({ ...fields, title: body2.title }, contextFromPrincipal(principal, body2));
|
|
59946
|
-
return
|
|
61680
|
+
return json6({ task: task3, created: true }, 201);
|
|
59947
61681
|
}
|
|
59948
61682
|
try {
|
|
59949
61683
|
const task3 = await store.tasks.update(existing.id, { ...fields, version: existing.version }, contextFromPrincipal(principal, body2));
|
|
59950
|
-
return
|
|
61684
|
+
return json6({ task: task3, created: false });
|
|
59951
61685
|
} catch (e) {
|
|
59952
61686
|
const msg = e.message || "";
|
|
59953
61687
|
if (msg.includes("version conflict"))
|
|
@@ -59995,7 +61729,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
59995
61729
|
const tasks = await store.tasks.list(filter);
|
|
59996
61730
|
const { limit: _l, offset: _o, ...countFilter } = filter;
|
|
59997
61731
|
const total = await store.tasks.count(countFilter);
|
|
59998
|
-
return
|
|
61732
|
+
return json6({ tasks, count: tasks.length, total });
|
|
59999
61733
|
}
|
|
60000
61734
|
if (method === "POST") {
|
|
60001
61735
|
const body2 = await readJson3(req);
|
|
@@ -60020,7 +61754,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60020
61754
|
if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== (body2.parent_id ?? null) || (persisted.plan_id ?? null) !== (body2.plan_id ?? null) || body2.created_by !== undefined && persisted.created_by !== body2.created_by) {
|
|
60021
61755
|
return error(500, "TASK_CREATE_PERSISTENCE_UNVERIFIED: task create was acknowledged but authoritative readback did not return the same stored task id, parent_id, plan_id, and explicit created_by", { code: "TASK_CREATE_PERSISTENCE_UNVERIFIED" });
|
|
60022
61756
|
}
|
|
60023
|
-
return
|
|
61757
|
+
return json6({ task: persisted }, 201);
|
|
60024
61758
|
}
|
|
60025
61759
|
return error(405, `method ${method} not allowed on /v1/tasks`);
|
|
60026
61760
|
}
|
|
@@ -60062,7 +61796,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60062
61796
|
new_holder: body3.new_holder,
|
|
60063
61797
|
reason: body3.reason
|
|
60064
61798
|
}, contextFromPrincipal(principal));
|
|
60065
|
-
return
|
|
61799
|
+
return json6({ receipt });
|
|
60066
61800
|
}
|
|
60067
61801
|
if (action === "comments") {
|
|
60068
61802
|
if (method === "GET") {
|
|
@@ -60076,7 +61810,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60076
61810
|
if (legacyPage.length > LEGACY_COMMENT_RESPONSE_LIMIT) {
|
|
60077
61811
|
return error(426, "task has too many comments for this client; upgrade @hasna/todos to use cursor pagination");
|
|
60078
61812
|
}
|
|
60079
|
-
return
|
|
61813
|
+
return json6({
|
|
60080
61814
|
comments: legacyPage,
|
|
60081
61815
|
count: legacyPage.length,
|
|
60082
61816
|
has_more: false,
|
|
@@ -60101,7 +61835,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60101
61835
|
const page = (await store.audit.getCommentsPage(id, { limit: limit + 1, ...before ? { before } : {} }, contextFromPrincipal(principal))).map(redactComment3);
|
|
60102
61836
|
const hasMore = page.length > limit;
|
|
60103
61837
|
const comments = hasMore ? page.slice(1) : page;
|
|
60104
|
-
return
|
|
61838
|
+
return json6({
|
|
60105
61839
|
comments,
|
|
60106
61840
|
count: comments.length,
|
|
60107
61841
|
has_more: hasMore,
|
|
@@ -60124,7 +61858,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60124
61858
|
type: body3.type,
|
|
60125
61859
|
progress_pct: body3.progress_pct
|
|
60126
61860
|
}, contextFromPrincipal(principal, body3));
|
|
60127
|
-
return
|
|
61861
|
+
return json6({ comment: redactComment3(comment2) }, 201);
|
|
60128
61862
|
}
|
|
60129
61863
|
return error(405, `method ${method} not allowed on /v1/tasks/:id/comments`);
|
|
60130
61864
|
}
|
|
@@ -60134,7 +61868,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60134
61868
|
if (!await store.tasks.get(id))
|
|
60135
61869
|
return error(404, "task not found");
|
|
60136
61870
|
const history = await store.audit.getTaskHistory(id);
|
|
60137
|
-
return
|
|
61871
|
+
return json6({ history, count: history.length });
|
|
60138
61872
|
}
|
|
60139
61873
|
if (action === "lock" || action === "unlock") {
|
|
60140
61874
|
if (method !== "POST")
|
|
@@ -60146,7 +61880,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60146
61880
|
if (typeof store.tasks.lock !== "function")
|
|
60147
61881
|
return error(501, "task locking is not supported by this storage backend");
|
|
60148
61882
|
const agentId3 = body3.agent_id || principal.agent || "todos-serve";
|
|
60149
|
-
return
|
|
61883
|
+
return json6({ result: await store.tasks.lock(id, agentId3) });
|
|
60150
61884
|
}
|
|
60151
61885
|
if (typeof store.tasks.unlock !== "function")
|
|
60152
61886
|
return error(501, "task unlocking is not supported by this storage backend");
|
|
@@ -60154,7 +61888,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60154
61888
|
if (!principal.scopes.includes("todos:*"))
|
|
60155
61889
|
return error(403, "force unlock requires todos:* scope");
|
|
60156
61890
|
const released2 = await store.tasks.unlock(id);
|
|
60157
|
-
return
|
|
61891
|
+
return json6({ success: released2 });
|
|
60158
61892
|
}
|
|
60159
61893
|
if (body3.agent_id && body3.agent_id !== principal.agent && !principal.scopes.includes("todos:*")) {
|
|
60160
61894
|
return error(403, "unlock agent_id must match the authenticated agent");
|
|
@@ -60163,7 +61897,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60163
61897
|
if (!agentId2)
|
|
60164
61898
|
return error(403, "unlock requires an agent-bound key or force=true");
|
|
60165
61899
|
const released = await store.tasks.unlock(id, agentId2);
|
|
60166
|
-
return
|
|
61900
|
+
return json6({ success: released });
|
|
60167
61901
|
}
|
|
60168
61902
|
if (action === "dependencies") {
|
|
60169
61903
|
if (!store.dependencies)
|
|
@@ -60172,7 +61906,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60172
61906
|
if (!await store.tasks.get(id))
|
|
60173
61907
|
return error(404, "task not found");
|
|
60174
61908
|
const edges = await store.dependencies.list(id);
|
|
60175
|
-
return
|
|
61909
|
+
return json6(edges);
|
|
60176
61910
|
}
|
|
60177
61911
|
if (method === "POST") {
|
|
60178
61912
|
const body3 = await readJson3(req) ?? {};
|
|
@@ -60181,7 +61915,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60181
61915
|
}
|
|
60182
61916
|
try {
|
|
60183
61917
|
const dependency2 = await store.dependencies.add(id, body3.depends_on, contextFromPrincipal(principal));
|
|
60184
|
-
return
|
|
61918
|
+
return json6({ dependency: dependency2 }, 201);
|
|
60185
61919
|
} catch (e) {
|
|
60186
61920
|
const msg = e.message || "";
|
|
60187
61921
|
if (msg.includes("not found"))
|
|
@@ -60195,7 +61929,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60195
61929
|
if (!subId)
|
|
60196
61930
|
return error(400, "dependency target id is required (/v1/tasks/:id/dependencies/:dep)");
|
|
60197
61931
|
const removed = await store.dependencies.remove(id, subId);
|
|
60198
|
-
return
|
|
61932
|
+
return json6({ removed });
|
|
60199
61933
|
}
|
|
60200
61934
|
return error(405, `method ${method} not allowed on /v1/tasks/:id/dependencies`);
|
|
60201
61935
|
}
|
|
@@ -60206,7 +61940,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60206
61940
|
if (!await store.tasks.get(id))
|
|
60207
61941
|
return error(404, "task not found");
|
|
60208
61942
|
const verifications = await store.verifications.list(id);
|
|
60209
|
-
return
|
|
61943
|
+
return json6({ verifications, count: verifications.length });
|
|
60210
61944
|
}
|
|
60211
61945
|
if (method === "POST") {
|
|
60212
61946
|
const body3 = await readJson3(req) ?? {};
|
|
@@ -60222,7 +61956,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60222
61956
|
artifact_path: body3.artifact_path,
|
|
60223
61957
|
agent_id: body3.agent_id
|
|
60224
61958
|
}, contextFromPrincipal(principal, body3));
|
|
60225
|
-
return
|
|
61959
|
+
return json6({ verification: verification2 }, 201);
|
|
60226
61960
|
} catch (e) {
|
|
60227
61961
|
const msg = e.message || "";
|
|
60228
61962
|
if (msg.includes("not found"))
|
|
@@ -60239,7 +61973,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60239
61973
|
if (!await store.tasks.get(id))
|
|
60240
61974
|
return error(404, "task not found");
|
|
60241
61975
|
const commits = await store.commits.list(id);
|
|
60242
|
-
return
|
|
61976
|
+
return json6({ commits, count: commits.length });
|
|
60243
61977
|
}
|
|
60244
61978
|
if (method === "POST") {
|
|
60245
61979
|
const body3 = await readJson3(req) ?? {};
|
|
@@ -60253,7 +61987,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60253
61987
|
author: body3.author,
|
|
60254
61988
|
files_changed: Array.isArray(body3.files_changed) ? body3.files_changed : undefined
|
|
60255
61989
|
}, contextFromPrincipal(principal));
|
|
60256
|
-
return
|
|
61990
|
+
return json6({ commit }, 201);
|
|
60257
61991
|
} catch (e) {
|
|
60258
61992
|
const msg = e.message || "";
|
|
60259
61993
|
if (msg.includes("not found"))
|
|
@@ -60270,7 +62004,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60270
62004
|
if (!await store.tasks.get(id))
|
|
60271
62005
|
return error(404, "task not found");
|
|
60272
62006
|
const refs = await store.gitRefs.list(id);
|
|
60273
|
-
return
|
|
62007
|
+
return json6({ refs, count: refs.length });
|
|
60274
62008
|
}
|
|
60275
62009
|
if (method === "POST") {
|
|
60276
62010
|
const body3 = await readJson3(req) ?? {};
|
|
@@ -60286,7 +62020,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60286
62020
|
provider: body3.provider,
|
|
60287
62021
|
metadata: body3.metadata
|
|
60288
62022
|
}, contextFromPrincipal(principal));
|
|
60289
|
-
return
|
|
62023
|
+
return json6({ ref }, 201);
|
|
60290
62024
|
} catch (e) {
|
|
60291
62025
|
const msg = e.message || "";
|
|
60292
62026
|
if (msg.includes("not found"))
|
|
@@ -60302,13 +62036,13 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60302
62036
|
const body2 = actionJson.value && typeof actionJson.value === "object" && !Array.isArray(actionJson.value) ? actionJson.value : {};
|
|
60303
62037
|
const agentId = typeof body2.agent_id === "string" ? body2.agent_id : principal.agent || "todos-serve";
|
|
60304
62038
|
if (action === "start" && method === "POST") {
|
|
60305
|
-
return
|
|
62039
|
+
return json6({ task: await store.tasks.start(id, agentId) });
|
|
60306
62040
|
}
|
|
60307
62041
|
if (action === "complete" && method === "POST") {
|
|
60308
62042
|
const parsed = validateTaskCompletion(actionJson.value);
|
|
60309
62043
|
if (!parsed.ok)
|
|
60310
62044
|
return error(400, parsed.message);
|
|
60311
|
-
return
|
|
62045
|
+
return json6({
|
|
60312
62046
|
task: await store.tasks.complete(id, parsed.agentId || principal.agent || "todos-serve", parsed.options, contextFromPrincipal(principal, body2))
|
|
60313
62047
|
});
|
|
60314
62048
|
}
|
|
@@ -60316,12 +62050,12 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60316
62050
|
const parsed = validateTaskFailure(actionJson.value);
|
|
60317
62051
|
if (!parsed.ok)
|
|
60318
62052
|
return error(400, parsed.message);
|
|
60319
|
-
return
|
|
62053
|
+
return json6({
|
|
60320
62054
|
result: await store.tasks.fail(id, parsed.agentId || principal.agent || "todos-serve", parsed.reason, { retry: parsed.retry }, contextFromPrincipal(principal, body2))
|
|
60321
62055
|
});
|
|
60322
62056
|
}
|
|
60323
62057
|
if (action === "claim" && method === "POST") {
|
|
60324
|
-
return
|
|
62058
|
+
return json6({ task: await store.tasks.claimNext(agentId, {}) });
|
|
60325
62059
|
}
|
|
60326
62060
|
return error(404, `unknown task action: ${action}`);
|
|
60327
62061
|
}
|
|
@@ -60344,7 +62078,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60344
62078
|
throw e;
|
|
60345
62079
|
}
|
|
60346
62080
|
}
|
|
60347
|
-
return task3 ?
|
|
62081
|
+
return task3 ? json6({ task: task3 }) : error(404, "task not found");
|
|
60348
62082
|
}
|
|
60349
62083
|
if (method === "PATCH" || method === "PUT") {
|
|
60350
62084
|
const rawBody = await readJson3(req);
|
|
@@ -60363,7 +62097,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60363
62097
|
};
|
|
60364
62098
|
try {
|
|
60365
62099
|
const task3 = await store.tasks.update(id, patch);
|
|
60366
|
-
return task3 ?
|
|
62100
|
+
return task3 ? json6({ task: task3 }) : error(404, "task not found");
|
|
60367
62101
|
} catch (e) {
|
|
60368
62102
|
const msg = e.message || "";
|
|
60369
62103
|
if (msg.includes("version conflict"))
|
|
@@ -60373,7 +62107,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60373
62107
|
}
|
|
60374
62108
|
if (method === "DELETE") {
|
|
60375
62109
|
await store.tasks.delete(id, contextFromPrincipal(principal));
|
|
60376
|
-
return
|
|
62110
|
+
return json6({ deleted: true, id });
|
|
60377
62111
|
}
|
|
60378
62112
|
return error(405, `method ${method} not allowed on /v1/tasks/:id`);
|
|
60379
62113
|
}
|
|
@@ -60381,7 +62115,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60381
62115
|
if (!id) {
|
|
60382
62116
|
if (method === "GET") {
|
|
60383
62117
|
const projects = await store.projects.list();
|
|
60384
|
-
return
|
|
62118
|
+
return json6({ projects, count: projects.length });
|
|
60385
62119
|
}
|
|
60386
62120
|
if (method === "POST") {
|
|
60387
62121
|
const body2 = await readJson3(req);
|
|
@@ -60391,13 +62125,13 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60391
62125
|
if (!validated.ok)
|
|
60392
62126
|
return error(400, validated.message);
|
|
60393
62127
|
const project = await store.projects.create(validated.input, contextFromPrincipal(principal));
|
|
60394
|
-
return
|
|
62128
|
+
return json6({ project }, 201);
|
|
60395
62129
|
}
|
|
60396
62130
|
return error(405, `method ${method} not allowed on /v1/projects`);
|
|
60397
62131
|
}
|
|
60398
62132
|
if (action === "task-list" && subId === "ensure") {
|
|
60399
62133
|
if (method === "GET") {
|
|
60400
|
-
return
|
|
62134
|
+
return json6(await planProjectTaskListEnsure(store, id));
|
|
60401
62135
|
}
|
|
60402
62136
|
if (method !== "POST") {
|
|
60403
62137
|
return error(405, `method ${method} not allowed on /v1/projects/:id/task-list/ensure`);
|
|
@@ -60418,7 +62152,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60418
62152
|
expected_project_revision: body2.expected_project_revision,
|
|
60419
62153
|
...typeof body2.idempotency_key === "string" ? { idempotency_key: body2.idempotency_key } : {}
|
|
60420
62154
|
});
|
|
60421
|
-
return
|
|
62155
|
+
return json6(result, result.action === "created" ? 201 : 200);
|
|
60422
62156
|
}
|
|
60423
62157
|
if (action === "task-list" && subId === "rollback") {
|
|
60424
62158
|
if (method !== "POST") {
|
|
@@ -60436,7 +62170,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60436
62170
|
if (typeof body2.expected_task_list_revision !== "string" || !body2.expected_task_list_revision.trim()) {
|
|
60437
62171
|
return error(400, "expected_task_list_revision must be a non-empty string from the accepted receipt");
|
|
60438
62172
|
}
|
|
60439
|
-
return
|
|
62173
|
+
return json6(await rollbackProjectTaskListEnsure(store, id, {
|
|
60440
62174
|
receipt_id: body2.receipt_id,
|
|
60441
62175
|
expected_task_list_revision: body2.expected_task_list_revision
|
|
60442
62176
|
}));
|
|
@@ -60454,11 +62188,11 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60454
62188
|
const unknownField = Object.keys(body2).find((key2) => !["new_slug", "name"].includes(key2));
|
|
60455
62189
|
if (unknownField)
|
|
60456
62190
|
return error(400, `unknown project rename field: ${unknownField}`);
|
|
60457
|
-
return
|
|
62191
|
+
return json6(await store.projects.rename(id, body2, contextFromPrincipal(principal)));
|
|
60458
62192
|
}
|
|
60459
62193
|
if (method === "GET") {
|
|
60460
62194
|
const project = await store.projects.get(id);
|
|
60461
|
-
return project ?
|
|
62195
|
+
return project ? json6({ project }) : error(404, "project not found");
|
|
60462
62196
|
}
|
|
60463
62197
|
if (method === "PATCH" || method === "PUT") {
|
|
60464
62198
|
const body2 = await readJson3(req);
|
|
@@ -60470,18 +62204,18 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60470
62204
|
if (!await store.projects.get(id))
|
|
60471
62205
|
return error(404, "project not found");
|
|
60472
62206
|
const project = await store.projects.update(id, validated.patch);
|
|
60473
|
-
return
|
|
62207
|
+
return json6({ project });
|
|
60474
62208
|
}
|
|
60475
62209
|
if (method === "DELETE") {
|
|
60476
62210
|
await store.projects.delete(id, contextFromPrincipal(principal));
|
|
60477
|
-
return
|
|
62211
|
+
return json6({ deleted: true, id });
|
|
60478
62212
|
}
|
|
60479
62213
|
return error(405, `method ${method} not allowed on /v1/projects/:id`);
|
|
60480
62214
|
}
|
|
60481
62215
|
if (resource === "plans") {
|
|
60482
62216
|
if (!id && method === "GET") {
|
|
60483
62217
|
const plans = await store.plans.list(url.searchParams.get("project_id") ?? undefined);
|
|
60484
|
-
return
|
|
62218
|
+
return json6({ plans, count: plans.length });
|
|
60485
62219
|
}
|
|
60486
62220
|
if (!id && method === "POST") {
|
|
60487
62221
|
const body2 = await readJson3(req);
|
|
@@ -60499,14 +62233,14 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60499
62233
|
}
|
|
60500
62234
|
}
|
|
60501
62235
|
const plan = await store.plans.create(validated.input, contextFromPrincipal(principal, validated.input));
|
|
60502
|
-
return
|
|
62236
|
+
return json6({ plan }, 201);
|
|
60503
62237
|
}
|
|
60504
62238
|
if (id && action === "project-link" && !subId) {
|
|
60505
62239
|
if (method === "GET") {
|
|
60506
62240
|
const projectId = url.searchParams.get("project_id");
|
|
60507
62241
|
if (!projectId?.trim())
|
|
60508
62242
|
return error(400, "project_id query parameter is required");
|
|
60509
|
-
return
|
|
62243
|
+
return json6(await planPlanProjectLink(store, id, projectId));
|
|
60510
62244
|
}
|
|
60511
62245
|
if (method !== "POST")
|
|
60512
62246
|
return error(405, `method ${method} not allowed on /v1/plans/:id/project-link`);
|
|
@@ -60527,7 +62261,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60527
62261
|
expected_project_revision: body2.expected_project_revision,
|
|
60528
62262
|
idempotency_key: body2.idempotency_key
|
|
60529
62263
|
});
|
|
60530
|
-
return
|
|
62264
|
+
return json6(result, result.action === "linked" ? 201 : 200);
|
|
60531
62265
|
}
|
|
60532
62266
|
if (id && action === "project-link" && subId === "rollback") {
|
|
60533
62267
|
if (method !== "POST")
|
|
@@ -60544,14 +62278,69 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60544
62278
|
return error(400, `${field} must be a non-empty string`);
|
|
60545
62279
|
}
|
|
60546
62280
|
}
|
|
60547
|
-
return
|
|
62281
|
+
return json6(await rollbackPlanProjectLink(store, id, body2.project_id, {
|
|
60548
62282
|
receipt_id: body2.receipt_id,
|
|
60549
62283
|
expected_plan_revision: body2.expected_plan_revision
|
|
60550
62284
|
}));
|
|
60551
62285
|
}
|
|
62286
|
+
if (id && action === "comments") {
|
|
62287
|
+
if (!await store.plans.get(id))
|
|
62288
|
+
return error(404, "plan not found");
|
|
62289
|
+
if (method === "GET") {
|
|
62290
|
+
if (store.plans.getCommentsPage) {
|
|
62291
|
+
const rawLimit = url.searchParams.get("limit");
|
|
62292
|
+
const cursor = url.searchParams.get("cursor");
|
|
62293
|
+
const limit = rawLimit === null ? DEFAULT_COMMENT_PAGE_SIZE : Number(rawLimit);
|
|
62294
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_COMMENT_PAGE_SIZE) {
|
|
62295
|
+
return error(400, `limit must be an integer between 1 and ${MAX_COMMENT_PAGE_SIZE}`);
|
|
62296
|
+
}
|
|
62297
|
+
let before;
|
|
62298
|
+
if (cursor) {
|
|
62299
|
+
try {
|
|
62300
|
+
before = decodeCommentCursor(cursor);
|
|
62301
|
+
} catch {
|
|
62302
|
+
return error(400, "invalid comment cursor");
|
|
62303
|
+
}
|
|
62304
|
+
}
|
|
62305
|
+
const page = (await store.plans.getCommentsPage(id, { limit: limit + 1, ...before ? { before } : {} }, contextFromPrincipal(principal))).map(redactPlanComment2);
|
|
62306
|
+
const hasMore = page.length > limit;
|
|
62307
|
+
const comments2 = hasMore ? page.slice(1) : page;
|
|
62308
|
+
return json6({
|
|
62309
|
+
comments: comments2,
|
|
62310
|
+
count: comments2.length,
|
|
62311
|
+
has_more: hasMore,
|
|
62312
|
+
next_cursor: hasMore && comments2[0] ? encodeCommentCursor(comments2[0]) : null
|
|
62313
|
+
});
|
|
62314
|
+
}
|
|
62315
|
+
const comments = (await store.plans.getComments?.(id, contextFromPrincipal(principal)) ?? []).map(redactPlanComment2);
|
|
62316
|
+
return json6({ comments, count: comments.length });
|
|
62317
|
+
}
|
|
62318
|
+
if (method === "POST") {
|
|
62319
|
+
const body2 = await readJson3(req) ?? {};
|
|
62320
|
+
if (typeof body2.content !== "string" || !body2.content.trim()) {
|
|
62321
|
+
return error(400, "content is required");
|
|
62322
|
+
}
|
|
62323
|
+
const target = await store.plans.get(id);
|
|
62324
|
+
if (!target)
|
|
62325
|
+
return error(404, "plan not found");
|
|
62326
|
+
if (!store.plans.addComment) {
|
|
62327
|
+
return error(501, "plan comments are not supported by this storage backend");
|
|
62328
|
+
}
|
|
62329
|
+
const comment2 = await store.plans.addComment({
|
|
62330
|
+
plan_id: id,
|
|
62331
|
+
content: body2.content,
|
|
62332
|
+
agent_id: body2.agent_id ?? principal.agent ?? undefined,
|
|
62333
|
+
session_id: body2.session_id,
|
|
62334
|
+
type: body2.type,
|
|
62335
|
+
progress_pct: body2.progress_pct
|
|
62336
|
+
}, contextFromPrincipal(principal, body2));
|
|
62337
|
+
return json6({ comment: redactPlanComment2(comment2) }, 201);
|
|
62338
|
+
}
|
|
62339
|
+
return error(405, `method ${method} not allowed on /v1/plans/:id/comments`);
|
|
62340
|
+
}
|
|
60552
62341
|
if (id && method === "GET") {
|
|
60553
62342
|
const plan = await store.plans.get(id);
|
|
60554
|
-
return plan ?
|
|
62343
|
+
return plan ? json6({ plan }) : error(404, "plan not found");
|
|
60555
62344
|
}
|
|
60556
62345
|
if (id && (method === "PATCH" || method === "PUT")) {
|
|
60557
62346
|
const body2 = await readJson3(req);
|
|
@@ -60591,12 +62380,12 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60591
62380
|
}
|
|
60592
62381
|
}
|
|
60593
62382
|
const plan = await store.plans.update(id, body2);
|
|
60594
|
-
return
|
|
62383
|
+
return json6({ plan });
|
|
60595
62384
|
}
|
|
60596
62385
|
if (id && method === "DELETE") {
|
|
60597
62386
|
if (!await store.plans.delete(id, contextFromPrincipal(principal)))
|
|
60598
62387
|
return error(404, "plan not found");
|
|
60599
|
-
return
|
|
62388
|
+
return json6({ deleted: true, id });
|
|
60600
62389
|
}
|
|
60601
62390
|
if (id)
|
|
60602
62391
|
return error(405, `method ${method} not allowed on /v1/plans/:id`);
|
|
@@ -60605,7 +62394,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60605
62394
|
if (!id && method === "GET") {
|
|
60606
62395
|
const projectId = url.searchParams.get("project_id");
|
|
60607
62396
|
const templates = (await store.templates.list()).filter((template) => projectId === null || template.project_id === projectId);
|
|
60608
|
-
return
|
|
62397
|
+
return json6({ templates, count: templates.length });
|
|
60609
62398
|
}
|
|
60610
62399
|
if (!id && method === "POST") {
|
|
60611
62400
|
const body2 = await readJson3(req);
|
|
@@ -60613,13 +62402,13 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60613
62402
|
if (!validated.ok)
|
|
60614
62403
|
return error(400, validated.message);
|
|
60615
62404
|
const template = await store.templates.create(validated.input, contextFromPrincipal(principal));
|
|
60616
|
-
return
|
|
62405
|
+
return json6({ template: await store.templates.getWithTasks(template.id) }, 201);
|
|
60617
62406
|
}
|
|
60618
62407
|
if (!id)
|
|
60619
62408
|
return error(405, `method ${method} not allowed on /v1/templates`);
|
|
60620
62409
|
if (method === "GET") {
|
|
60621
62410
|
const template = await store.templates.getWithTasks(id);
|
|
60622
|
-
return template ?
|
|
62411
|
+
return template ? json6({ template }) : error(404, "template not found");
|
|
60623
62412
|
}
|
|
60624
62413
|
if (method === "PATCH" || method === "PUT") {
|
|
60625
62414
|
const body2 = await readJson3(req);
|
|
@@ -60627,18 +62416,18 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60627
62416
|
if (!validated.ok)
|
|
60628
62417
|
return error(400, validated.message);
|
|
60629
62418
|
const template = await store.templates.update(id, validated.patch, contextFromPrincipal(principal));
|
|
60630
|
-
return template ?
|
|
62419
|
+
return template ? json6({ template: await store.templates.getWithTasks(id) }) : error(404, "template not found");
|
|
60631
62420
|
}
|
|
60632
62421
|
if (method === "DELETE") {
|
|
60633
62422
|
const deleted = await store.templates.delete(id, contextFromPrincipal(principal));
|
|
60634
|
-
return deleted ?
|
|
62423
|
+
return deleted ? json6({ deleted: true, id }) : error(404, "template not found");
|
|
60635
62424
|
}
|
|
60636
62425
|
return error(405, `method ${method} not allowed on /v1/templates/:id`);
|
|
60637
62426
|
}
|
|
60638
62427
|
if (resource === "agents") {
|
|
60639
62428
|
if (!id && method === "GET") {
|
|
60640
62429
|
const agents = await store.agents.list();
|
|
60641
|
-
return
|
|
62430
|
+
return json6({ agents, count: agents.length });
|
|
60642
62431
|
}
|
|
60643
62432
|
if (!id && method === "POST") {
|
|
60644
62433
|
const body2 = await readJson3(req);
|
|
@@ -60648,7 +62437,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60648
62437
|
if (result && typeof result === "object" && "conflict" in result) {
|
|
60649
62438
|
return error(409, result.message ?? "agent name conflict", { conflict: true });
|
|
60650
62439
|
}
|
|
60651
|
-
return
|
|
62440
|
+
return json6({ agent: result }, 201);
|
|
60652
62441
|
}
|
|
60653
62442
|
if (id && action === "heartbeat") {
|
|
60654
62443
|
if (method !== "POST")
|
|
@@ -60657,7 +62446,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60657
62446
|
return error(501, "agent heartbeat is not supported by this storage backend");
|
|
60658
62447
|
}
|
|
60659
62448
|
const agent = await store.agents.heartbeat(id, contextFromPrincipal(principal));
|
|
60660
|
-
return agent ?
|
|
62449
|
+
return agent ? json6({ agent }) : error(404, "agent not found");
|
|
60661
62450
|
}
|
|
60662
62451
|
if (id && action === "release") {
|
|
60663
62452
|
if (method !== "POST")
|
|
@@ -60672,11 +62461,11 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60672
62461
|
if (!result.released) {
|
|
60673
62462
|
return error(409, "release denied: session_id does not match agent's current session", { released: false });
|
|
60674
62463
|
}
|
|
60675
|
-
return
|
|
62464
|
+
return json6({ agent: result.agent, released: true });
|
|
60676
62465
|
}
|
|
60677
62466
|
if (id && method === "GET") {
|
|
60678
62467
|
const agent = await store.agents.get(id);
|
|
60679
|
-
return agent ?
|
|
62468
|
+
return agent ? json6({ agent }) : error(404, "agent not found");
|
|
60680
62469
|
}
|
|
60681
62470
|
}
|
|
60682
62471
|
if (resource === "activity" && !id) {
|
|
@@ -60685,13 +62474,13 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60685
62474
|
const limitParam = url.searchParams.get("limit");
|
|
60686
62475
|
const limit = limitParam ? Math.max(1, Math.min(1e4, Number(limitParam) || 50)) : 50;
|
|
60687
62476
|
const activity = await store.audit.getRecentActivity(limit);
|
|
60688
|
-
return
|
|
62477
|
+
return json6({ activity, count: activity.length });
|
|
60689
62478
|
}
|
|
60690
62479
|
if (resource === "task-lists") {
|
|
60691
62480
|
if (!id && method === "GET") {
|
|
60692
62481
|
const projectId = url.searchParams.get("project_id") ?? undefined;
|
|
60693
62482
|
const taskLists = await store.taskLists.list(projectId);
|
|
60694
|
-
return
|
|
62483
|
+
return json6({ task_lists: taskLists, count: taskLists.length });
|
|
60695
62484
|
}
|
|
60696
62485
|
if (!id && method === "POST") {
|
|
60697
62486
|
const body2 = await readJson3(req);
|
|
@@ -60713,17 +62502,17 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60713
62502
|
return error(400, "task-list slug must be non-empty kebab-case");
|
|
60714
62503
|
}
|
|
60715
62504
|
const taskList = await store.taskLists.create(body2, contextFromPrincipal(principal));
|
|
60716
|
-
return
|
|
62505
|
+
return json6({ task_list: taskList }, 201);
|
|
60717
62506
|
}
|
|
60718
62507
|
if (id && method === "GET") {
|
|
60719
62508
|
const taskList = await store.taskLists.get(id);
|
|
60720
|
-
return taskList ?
|
|
62509
|
+
return taskList ? json6({ task_list: taskList }) : error(404, "task list not found");
|
|
60721
62510
|
}
|
|
60722
62511
|
if (id && (method === "PATCH" || method === "PUT")) {
|
|
60723
62512
|
const body2 = await readJson3(req);
|
|
60724
62513
|
if (!body2)
|
|
60725
62514
|
return error(400, "invalid JSON body");
|
|
60726
|
-
const unknownField = Object.keys(body2).find((key2) => !["slug", "name", "description", "metadata"].includes(key2));
|
|
62515
|
+
const unknownField = Object.keys(body2).find((key2) => !["slug", "name", "description", "metadata", "project_id"].includes(key2));
|
|
60727
62516
|
if (unknownField)
|
|
60728
62517
|
return error(400, `unsupported task-list update field: ${unknownField}`);
|
|
60729
62518
|
if (Object.keys(body2).length === 0)
|
|
@@ -60737,14 +62526,26 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60737
62526
|
if (body2.metadata !== undefined && (!body2.metadata || typeof body2.metadata !== "object" || Array.isArray(body2.metadata))) {
|
|
60738
62527
|
return error(400, "metadata must be an object");
|
|
60739
62528
|
}
|
|
62529
|
+
if (body2.project_id !== undefined) {
|
|
62530
|
+
if (body2.project_id !== null && (typeof body2.project_id !== "string" || !body2.project_id.trim())) {
|
|
62531
|
+
return error(400, "project_id must be a non-empty string or null");
|
|
62532
|
+
}
|
|
62533
|
+
if (typeof body2.project_id === "string" && body2.project_id.trim() !== "") {
|
|
62534
|
+
const target = await store.projects.get(body2.project_id.trim());
|
|
62535
|
+
if (!target) {
|
|
62536
|
+
return error(400, `task-list project_id must reference a registered project: ${body2.project_id}`);
|
|
62537
|
+
}
|
|
62538
|
+
body2.project_id = target.id;
|
|
62539
|
+
}
|
|
62540
|
+
}
|
|
60740
62541
|
if (!await store.taskLists.get(id))
|
|
60741
62542
|
return error(404, "task list not found");
|
|
60742
62543
|
const taskList = await store.taskLists.update(id, body2);
|
|
60743
|
-
return
|
|
62544
|
+
return json6({ task_list: taskList });
|
|
60744
62545
|
}
|
|
60745
62546
|
if (id && method === "DELETE") {
|
|
60746
62547
|
const deleted = await store.taskLists.delete(id, contextFromPrincipal(principal));
|
|
60747
|
-
return deleted ?
|
|
62548
|
+
return deleted ? json6({ deleted: true, id }) : error(404, "task list not found");
|
|
60748
62549
|
}
|
|
60749
62550
|
return error(405, `method ${method} not allowed on /v1/task-lists${id ? "/:id" : ""}`);
|
|
60750
62551
|
}
|
|
@@ -60755,7 +62556,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60755
62556
|
return error(501, "dependency edge listing is not supported by this storage backend");
|
|
60756
62557
|
}
|
|
60757
62558
|
const dependencies2 = await store.dependencies.listAll();
|
|
60758
|
-
return
|
|
62559
|
+
return json6({ dependencies: dependencies2, count: dependencies2.length });
|
|
60759
62560
|
}
|
|
60760
62561
|
if (resource === "commits" && id) {
|
|
60761
62562
|
if (method !== "GET")
|
|
@@ -60763,7 +62564,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60763
62564
|
if (!store.commits)
|
|
60764
62565
|
return error(501, "commit links are not supported by this storage backend");
|
|
60765
62566
|
const commit = await store.commits.find(id);
|
|
60766
|
-
return
|
|
62567
|
+
return json6({ commit: commit ?? null });
|
|
60767
62568
|
}
|
|
60768
62569
|
if (resource === "refs" && id) {
|
|
60769
62570
|
if (method !== "GET")
|
|
@@ -60777,7 +62578,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60777
62578
|
return error(400, "ref path segment has invalid percent encoding");
|
|
60778
62579
|
}
|
|
60779
62580
|
const refs = await store.gitRefs.find(decodedRef);
|
|
60780
|
-
return
|
|
62581
|
+
return json6({ refs, count: refs.length });
|
|
60781
62582
|
}
|
|
60782
62583
|
if (resource === "next" && !id) {
|
|
60783
62584
|
if (method !== "GET")
|
|
@@ -60789,7 +62590,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60789
62590
|
...url.searchParams.get("plan_id") ? { plan_id: url.searchParams.get("plan_id") } : {}
|
|
60790
62591
|
};
|
|
60791
62592
|
const task3 = await store.tasks.getNext(agent, filters);
|
|
60792
|
-
return
|
|
62593
|
+
return json6({ task: task3 ?? null });
|
|
60793
62594
|
}
|
|
60794
62595
|
if (resource === "stats" && method === "GET") {
|
|
60795
62596
|
const [tasks, tasksAll, projects] = await Promise.all([
|
|
@@ -60797,7 +62598,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60797
62598
|
store.tasks.count({ include_subtasks: true }),
|
|
60798
62599
|
store.projects.list()
|
|
60799
62600
|
]);
|
|
60800
|
-
return
|
|
62601
|
+
return json6({ tasks, tasks_all: tasksAll, subtasks: tasksAll - tasks, projects: projects.length });
|
|
60801
62602
|
}
|
|
60802
62603
|
if (resource === "integrity" && !id) {
|
|
60803
62604
|
if (method !== "GET")
|
|
@@ -60806,7 +62607,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60806
62607
|
return error(501, "referential-integrity reporting is not supported by this storage backend");
|
|
60807
62608
|
}
|
|
60808
62609
|
const integrity = await store.integrity.report();
|
|
60809
|
-
return
|
|
62610
|
+
return json6({ integrity });
|
|
60810
62611
|
}
|
|
60811
62612
|
if (resource === "import") {
|
|
60812
62613
|
if (method !== "POST")
|
|
@@ -60831,7 +62632,7 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60831
62632
|
}
|
|
60832
62633
|
const operation = completionImports.operations[0];
|
|
60833
62634
|
const completed = await store.plans.completeAtRevision(operation.id, operation.expected_updated_at, contextFromPrincipal(principal));
|
|
60834
|
-
return
|
|
62635
|
+
return json6({
|
|
60835
62636
|
result: {
|
|
60836
62637
|
inserted: 0,
|
|
60837
62638
|
updated: completed.applied ? 1 : 0,
|
|
@@ -60870,17 +62671,17 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60870
62671
|
audit_history_id: failure.auditHistoryId
|
|
60871
62672
|
});
|
|
60872
62673
|
}
|
|
60873
|
-
return
|
|
62674
|
+
return json6({ result, received });
|
|
60874
62675
|
}
|
|
60875
62676
|
return error(404, `unknown /v1 resource: ${resource ?? "(root)"}`);
|
|
60876
62677
|
} catch (e) {
|
|
60877
62678
|
if (e instanceof PlanProjectLinkError) {
|
|
60878
|
-
const
|
|
60879
|
-
return error(
|
|
62679
|
+
const status3 = e.code === "PLAN_PROJECT_LINK_PLAN_NOT_FOUND" || e.code === "PLAN_PROJECT_LINK_PROJECT_NOT_FOUND" || e.code === "PLAN_PROJECT_LINK_RECEIPT_NOT_FOUND" ? 404 : e.code === "PLAN_PROJECT_LINK_IDEMPOTENCY_KEY_INVALID" ? 400 : e.code === "PLAN_PROJECT_LINK_UNSUPPORTED" ? 501 : 409;
|
|
62680
|
+
return error(status3, e.message, { code: e.code, conflict: status3 === 409, ...e.details });
|
|
60880
62681
|
}
|
|
60881
62682
|
if (e instanceof ProjectTaskListEnsureError) {
|
|
60882
|
-
const
|
|
60883
|
-
return error(
|
|
62683
|
+
const status3 = e.code === "PROJECT_NOT_FOUND" || e.code === "PROJECT_TASK_LIST_RECEIPT_NOT_FOUND" ? 404 : e.code === "PROJECT_TASK_LIST_IDEMPOTENCY_KEY_INVALID" ? 400 : 409;
|
|
62684
|
+
return error(status3, e.message, { code: e.code, conflict: status3 === 409, ...e.details });
|
|
60884
62685
|
}
|
|
60885
62686
|
if (e instanceof TaskReferenceAmbiguousError) {
|
|
60886
62687
|
return error(409, e.message, {
|
|
@@ -60902,10 +62703,10 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60902
62703
|
});
|
|
60903
62704
|
}
|
|
60904
62705
|
if (e instanceof StaleLockHandoffError) {
|
|
60905
|
-
const
|
|
60906
|
-
return error(
|
|
62706
|
+
const status3 = e.code === "STALE_LOCK_HANDOFF_INVALID_TASK_ID" || e.code === "STALE_LOCK_HANDOFF_INVALID_INPUT" ? 400 : e.code === "STALE_LOCK_HANDOFF_ACTOR_MISMATCH" ? 403 : 409;
|
|
62707
|
+
return error(status3, e.message, {
|
|
60907
62708
|
code: e.code,
|
|
60908
|
-
conflict:
|
|
62709
|
+
conflict: status3 === 409,
|
|
60909
62710
|
...e.details
|
|
60910
62711
|
});
|
|
60911
62712
|
}
|
|
@@ -60933,19 +62734,20 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
60933
62734
|
return error(500, e.message || "internal error");
|
|
60934
62735
|
}
|
|
60935
62736
|
}
|
|
60936
|
-
var
|
|
62737
|
+
var JSON_HEADERS5, DEFAULT_COMMENT_PAGE_SIZE = 100, MAX_COMMENT_PAGE_SIZE = 500, LEGACY_COMMENT_RESPONSE_LIMIT = 500, RFC3339_DATE_TIME;
|
|
60937
62738
|
var init_v1 = __esm(() => {
|
|
60938
62739
|
init_types();
|
|
60939
62740
|
init_cloud();
|
|
60940
62741
|
init_pr_groups();
|
|
60941
62742
|
init_project_registration();
|
|
60942
62743
|
init_task_manifest();
|
|
62744
|
+
init_task_subtree_transfer();
|
|
60943
62745
|
init_redaction();
|
|
60944
62746
|
init_project_task_list_ensure();
|
|
60945
62747
|
init_plan_project_link();
|
|
60946
62748
|
init_stale_lock_handoff();
|
|
60947
62749
|
init_audit_history_import();
|
|
60948
|
-
|
|
62750
|
+
JSON_HEADERS5 = { "Content-Type": "application/json" };
|
|
60949
62751
|
RFC3339_DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})[Tt]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$/;
|
|
60950
62752
|
});
|
|
60951
62753
|
|
|
@@ -61156,7 +62958,7 @@ class SqlitePrGroupLedgerPersistence {
|
|
|
61156
62958
|
}
|
|
61157
62959
|
}
|
|
61158
62960
|
var sqliteTransactionTails2;
|
|
61159
|
-
var
|
|
62961
|
+
var init_sqlite4 = __esm(() => {
|
|
61160
62962
|
sqliteTransactionTails2 = new WeakMap;
|
|
61161
62963
|
});
|
|
61162
62964
|
|
|
@@ -61187,10 +62989,10 @@ function createLocalPrGroupLedger(db = getDatabase()) {
|
|
|
61187
62989
|
var init_pr_groups2 = __esm(() => {
|
|
61188
62990
|
init_database();
|
|
61189
62991
|
init_ledger();
|
|
61190
|
-
|
|
62992
|
+
init_sqlite4();
|
|
61191
62993
|
init_types3();
|
|
61192
62994
|
init_ledger();
|
|
61193
|
-
|
|
62995
|
+
init_sqlite4();
|
|
61194
62996
|
init_http_client();
|
|
61195
62997
|
init_postgres();
|
|
61196
62998
|
});
|
|
@@ -61201,7 +63003,7 @@ __export(exports_serve, {
|
|
|
61201
63003
|
taskToSummary: () => taskToSummary,
|
|
61202
63004
|
startServer: () => startServer,
|
|
61203
63005
|
serveStaticFile: () => serveStaticFile,
|
|
61204
|
-
json: () =>
|
|
63006
|
+
json: () => json4,
|
|
61205
63007
|
checkAuth: () => checkAuth,
|
|
61206
63008
|
SECURITY_HEADERS: () => SECURITY_HEADERS,
|
|
61207
63009
|
MIME_TYPES: () => MIME_TYPES
|
|
@@ -61287,9 +63089,9 @@ function checkRateLimit(ip) {
|
|
|
61287
63089
|
}
|
|
61288
63090
|
return { allowed: true };
|
|
61289
63091
|
}
|
|
61290
|
-
function
|
|
63092
|
+
function json4(data, status3 = 200, headers) {
|
|
61291
63093
|
return new Response(JSON.stringify(data), {
|
|
61292
|
-
status:
|
|
63094
|
+
status: status3,
|
|
61293
63095
|
headers: {
|
|
61294
63096
|
"Content-Type": "application/json",
|
|
61295
63097
|
...SECURITY_HEADERS,
|
|
@@ -61432,7 +63234,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
61432
63234
|
"Access-Control-Allow-Headers": "Content-Type, X-API-Key, Authorization",
|
|
61433
63235
|
Vary: "Origin"
|
|
61434
63236
|
} : undefined;
|
|
61435
|
-
const jsonWithCors = (data,
|
|
63237
|
+
const jsonWithCors = (data, status3 = 200) => json4(data, status3, corsHeaders);
|
|
61436
63238
|
if (method === "OPTIONS") {
|
|
61437
63239
|
return new Response(null, {
|
|
61438
63240
|
headers: corsHeaders || {
|
|
@@ -61513,13 +63315,13 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
61513
63315
|
return res;
|
|
61514
63316
|
}
|
|
61515
63317
|
if (path === "/api/health" && method === "GET") {
|
|
61516
|
-
return handleHealth(ctx,
|
|
63318
|
+
return handleHealth(ctx, json4);
|
|
61517
63319
|
}
|
|
61518
63320
|
if (path === "/api/headless" && method === "GET") {
|
|
61519
|
-
return handleHeadlessBoundary(ctx,
|
|
63321
|
+
return handleHeadlessBoundary(ctx, json4);
|
|
61520
63322
|
}
|
|
61521
63323
|
if (path === "/api/stats" && method === "GET") {
|
|
61522
|
-
return handleStats(ctx,
|
|
63324
|
+
return handleStats(ctx, json4);
|
|
61523
63325
|
}
|
|
61524
63326
|
if (path === "/api/tasks" && method === "GET") {
|
|
61525
63327
|
return handleListTasks(req, url, ctx, jsonWithCors, taskToSummary);
|
|
@@ -61534,16 +63336,16 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
61534
63336
|
return handleTasksExport(req, url, ctx, jsonWithCors, taskToSummary);
|
|
61535
63337
|
}
|
|
61536
63338
|
if (path === "/api/tasks/bulk" && method === "POST") {
|
|
61537
|
-
return handleTasksBulk(req, ctx,
|
|
63339
|
+
return handleTasksBulk(req, ctx, json4);
|
|
61538
63340
|
}
|
|
61539
63341
|
if (path === "/api/tasks/status" && method === "GET") {
|
|
61540
|
-
return handleTasksStatus(req, url, ctx,
|
|
63342
|
+
return handleTasksStatus(req, url, ctx, json4);
|
|
61541
63343
|
}
|
|
61542
63344
|
if (path === "/api/tasks/next" && method === "GET") {
|
|
61543
63345
|
return handleTasksNext(req, url, ctx, jsonWithCors, taskToSummary);
|
|
61544
63346
|
}
|
|
61545
63347
|
if (path === "/api/tasks/active" && method === "GET") {
|
|
61546
|
-
return handleTasksActive(req, url, ctx,
|
|
63348
|
+
return handleTasksActive(req, url, ctx, json4);
|
|
61547
63349
|
}
|
|
61548
63350
|
if (path === "/api/tasks/stale" && method === "GET") {
|
|
61549
63351
|
return handleTasksStale(req, url, ctx, jsonWithCors, taskToSummary);
|
|
@@ -61556,11 +63358,11 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
61556
63358
|
}
|
|
61557
63359
|
const attachmentsMatch = path.match(/^\/api\/tasks\/([^/]+)\/attachments$/);
|
|
61558
63360
|
if (attachmentsMatch && method === "GET") {
|
|
61559
|
-
return handleTaskAttachments(attachmentsMatch[1], ctx,
|
|
63361
|
+
return handleTaskAttachments(attachmentsMatch[1], ctx, json4);
|
|
61560
63362
|
}
|
|
61561
63363
|
const progressMatch = path.match(/^\/api\/tasks\/([^/]+)\/progress$/);
|
|
61562
63364
|
if (progressMatch) {
|
|
61563
|
-
const res = await handleTaskProgress(progressMatch[1], req, method, ctx,
|
|
63365
|
+
const res = await handleTaskProgress(progressMatch[1], req, method, ctx, json4, url);
|
|
61564
63366
|
if (res !== null)
|
|
61565
63367
|
return res;
|
|
61566
63368
|
}
|
|
@@ -61574,7 +63376,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
61574
63376
|
return handlePatchTask(id, req, ctx, jsonWithCors, taskToSummary);
|
|
61575
63377
|
}
|
|
61576
63378
|
if (method === "DELETE") {
|
|
61577
|
-
return handleDeleteTask(id, ctx,
|
|
63379
|
+
return handleDeleteTask(id, ctx, json4);
|
|
61578
63380
|
}
|
|
61579
63381
|
}
|
|
61580
63382
|
const startMatch = path.match(/^\/api\/tasks\/([^/]+)\/start$/);
|
|
@@ -61590,7 +63392,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
61590
63392
|
return handleCompleteTask(completeMatch[1], ctx, jsonWithCors, taskToSummary);
|
|
61591
63393
|
}
|
|
61592
63394
|
if (path === "/api/projects" && method === "GET") {
|
|
61593
|
-
return handleListProjects(url, ctx,
|
|
63395
|
+
return handleListProjects(url, ctx, json4);
|
|
61594
63396
|
}
|
|
61595
63397
|
if (path === "/api/agents/me" && method === "GET") {
|
|
61596
63398
|
return handleAgentMe(req, url, ctx, jsonWithCors, taskToSummary);
|
|
@@ -61603,92 +63405,92 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
61603
63405
|
return handleClaimTask(req, ctx, jsonWithCors, taskToSummary);
|
|
61604
63406
|
}
|
|
61605
63407
|
if (path === "/api/orgs" && method === "GET") {
|
|
61606
|
-
return handleListOrgs(ctx,
|
|
63408
|
+
return handleListOrgs(ctx, json4);
|
|
61607
63409
|
}
|
|
61608
63410
|
if (path === "/api/orgs" && method === "POST") {
|
|
61609
|
-
return handleCreateOrg(req, ctx,
|
|
63411
|
+
return handleCreateOrg(req, ctx, json4);
|
|
61610
63412
|
}
|
|
61611
63413
|
const orgMatch = path.match(/^\/api\/orgs\/([^/]+)$/);
|
|
61612
63414
|
if (orgMatch && method === "PATCH") {
|
|
61613
|
-
return handleUpdateOrg(orgMatch[1], req, ctx,
|
|
63415
|
+
return handleUpdateOrg(orgMatch[1], req, ctx, json4);
|
|
61614
63416
|
}
|
|
61615
63417
|
if (orgMatch && method === "DELETE") {
|
|
61616
|
-
return handleDeleteOrg(orgMatch[1], ctx,
|
|
63418
|
+
return handleDeleteOrg(orgMatch[1], ctx, json4);
|
|
61617
63419
|
}
|
|
61618
63420
|
if (path === "/api/org" && method === "GET") {
|
|
61619
|
-
return handleOrgChart(ctx,
|
|
63421
|
+
return handleOrgChart(ctx, json4);
|
|
61620
63422
|
}
|
|
61621
63423
|
const teamMatch = path.match(/^\/api\/agents\/([^/]+)\/team$/);
|
|
61622
63424
|
if (teamMatch && method === "GET") {
|
|
61623
|
-
return handleAgentTeam(teamMatch[1], ctx,
|
|
63425
|
+
return handleAgentTeam(teamMatch[1], ctx, json4);
|
|
61624
63426
|
}
|
|
61625
63427
|
if (path === "/api/agents" && method === "GET") {
|
|
61626
|
-
return handleListAgents(url, ctx,
|
|
63428
|
+
return handleListAgents(url, ctx, json4);
|
|
61627
63429
|
}
|
|
61628
63430
|
if (path === "/api/projects" && method === "POST") {
|
|
61629
|
-
return handleCreateProject(req, ctx,
|
|
63431
|
+
return handleCreateProject(req, ctx, json4);
|
|
61630
63432
|
}
|
|
61631
63433
|
const projectDeleteMatch = path.match(/^\/api\/projects\/([^/]+)$/);
|
|
61632
63434
|
if (projectDeleteMatch && method === "DELETE") {
|
|
61633
|
-
return handleDeleteProject(projectDeleteMatch[1], ctx,
|
|
63435
|
+
return handleDeleteProject(projectDeleteMatch[1], ctx, json4);
|
|
61634
63436
|
}
|
|
61635
63437
|
if (path === "/api/agents" && method === "POST") {
|
|
61636
|
-
return handleRegisterAgent(req, ctx,
|
|
63438
|
+
return handleRegisterAgent(req, ctx, json4);
|
|
61637
63439
|
}
|
|
61638
63440
|
const agentMatch = path.match(/^\/api\/agents\/([^/]+)$/);
|
|
61639
63441
|
if (agentMatch && method === "PATCH") {
|
|
61640
|
-
return handleUpdateAgent(agentMatch[1], req, ctx,
|
|
63442
|
+
return handleUpdateAgent(agentMatch[1], req, ctx, json4);
|
|
61641
63443
|
}
|
|
61642
63444
|
if (agentMatch && method === "DELETE") {
|
|
61643
|
-
return handleDeleteAgent(agentMatch[1], ctx,
|
|
63445
|
+
return handleDeleteAgent(agentMatch[1], ctx, json4);
|
|
61644
63446
|
}
|
|
61645
63447
|
if (path === "/api/agents/bulk" && method === "POST") {
|
|
61646
|
-
return handleBulkDeleteAgents(req, ctx,
|
|
63448
|
+
return handleBulkDeleteAgents(req, ctx, json4);
|
|
61647
63449
|
}
|
|
61648
63450
|
if (path === "/api/projects/bulk" && method === "POST") {
|
|
61649
|
-
return handleBulkDeleteProjects(req, ctx,
|
|
63451
|
+
return handleBulkDeleteProjects(req, ctx, json4);
|
|
61650
63452
|
}
|
|
61651
63453
|
if (path === "/api/doctor" && method === "GET") {
|
|
61652
|
-
return handleDoctor(ctx,
|
|
63454
|
+
return handleDoctor(ctx, json4);
|
|
61653
63455
|
}
|
|
61654
63456
|
if (path === "/api/report" && method === "GET") {
|
|
61655
|
-
return handleReport(req, url, ctx,
|
|
63457
|
+
return handleReport(req, url, ctx, json4);
|
|
61656
63458
|
}
|
|
61657
63459
|
if (path === "/api/activity" && method === "GET") {
|
|
61658
|
-
return handleActivity(req, url, ctx,
|
|
63460
|
+
return handleActivity(req, url, ctx, json4);
|
|
61659
63461
|
}
|
|
61660
63462
|
const historyMatch = path.match(/^\/api\/tasks\/([^/]+)\/history$/);
|
|
61661
63463
|
if (historyMatch && method === "GET") {
|
|
61662
|
-
return handleTaskHistory(historyMatch[1], ctx,
|
|
63464
|
+
return handleTaskHistory(historyMatch[1], ctx, json4, url);
|
|
61663
63465
|
}
|
|
61664
63466
|
if (path === "/api/webhooks" && method === "GET") {
|
|
61665
|
-
return handleListWebhooks(ctx,
|
|
63467
|
+
return handleListWebhooks(ctx, json4);
|
|
61666
63468
|
}
|
|
61667
63469
|
if (path === "/api/webhooks" && method === "POST") {
|
|
61668
|
-
return handleCreateWebhook(req, ctx,
|
|
63470
|
+
return handleCreateWebhook(req, ctx, json4);
|
|
61669
63471
|
}
|
|
61670
63472
|
const webhookMatch = path.match(/^\/api\/webhooks\/([^/]+)$/);
|
|
61671
63473
|
if (webhookMatch && method === "DELETE") {
|
|
61672
|
-
return handleDeleteWebhook(webhookMatch[1], ctx,
|
|
63474
|
+
return handleDeleteWebhook(webhookMatch[1], ctx, json4);
|
|
61673
63475
|
}
|
|
61674
63476
|
if (path === "/api/templates" && method === "GET") {
|
|
61675
|
-
return handleListTemplates(ctx,
|
|
63477
|
+
return handleListTemplates(ctx, json4);
|
|
61676
63478
|
}
|
|
61677
63479
|
if (path === "/api/templates" && method === "POST") {
|
|
61678
|
-
return handleCreateTemplate(req, ctx,
|
|
63480
|
+
return handleCreateTemplate(req, ctx, json4);
|
|
61679
63481
|
}
|
|
61680
63482
|
const templateMatch = path.match(/^\/api\/templates\/([^/]+)$/);
|
|
61681
63483
|
if (templateMatch && method === "DELETE") {
|
|
61682
|
-
return handleDeleteTemplate(templateMatch[1], ctx,
|
|
63484
|
+
return handleDeleteTemplate(templateMatch[1], ctx, json4);
|
|
61683
63485
|
}
|
|
61684
63486
|
if (path === "/api/plans" && method === "GET") {
|
|
61685
|
-
return handleListPlans(url, ctx,
|
|
63487
|
+
return handleListPlans(url, ctx, json4);
|
|
61686
63488
|
}
|
|
61687
63489
|
if (path === "/api/plans" && method === "POST") {
|
|
61688
|
-
return handleCreatePlan(req, ctx,
|
|
63490
|
+
return handleCreatePlan(req, ctx, json4);
|
|
61689
63491
|
}
|
|
61690
63492
|
if (path === "/api/plans/bulk" && method === "POST") {
|
|
61691
|
-
return handleBulkDeletePlans(req, ctx,
|
|
63493
|
+
return handleBulkDeletePlans(req, ctx, json4);
|
|
61692
63494
|
}
|
|
61693
63495
|
const planMatch = path.match(/^\/api\/plans\/([^/]+)$/);
|
|
61694
63496
|
if (planMatch) {
|
|
@@ -61697,16 +63499,16 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
61697
63499
|
return handleGetPlan(id, ctx, jsonWithCors, taskToSummary);
|
|
61698
63500
|
}
|
|
61699
63501
|
if (method === "PATCH") {
|
|
61700
|
-
return handleUpdatePlan(id, req, ctx,
|
|
63502
|
+
return handleUpdatePlan(id, req, ctx, json4);
|
|
61701
63503
|
}
|
|
61702
63504
|
if (method === "DELETE") {
|
|
61703
|
-
return handleDeletePlan(id, ctx,
|
|
63505
|
+
return handleDeletePlan(id, ctx, json4);
|
|
61704
63506
|
}
|
|
61705
63507
|
}
|
|
61706
63508
|
const staticRes = handleStaticFiles(path, method, ctx, jsonWithCors, serveStaticFile);
|
|
61707
63509
|
if (staticRes)
|
|
61708
63510
|
return staticRes;
|
|
61709
|
-
return
|
|
63511
|
+
return json4({ error: "Not found" }, 404);
|
|
61710
63512
|
}
|
|
61711
63513
|
});
|
|
61712
63514
|
const shutdown = () => {
|