@hasna/todos 0.15.35 → 0.15.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +3430 -827
- package/dist/cli/stage-a.d.ts.map +1 -1
- package/dist/contracts.js +66 -58
- 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-graph.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 +1897 -334
- 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/lib/instant-compare.d.ts +35 -0
- package/dist/lib/instant-compare.d.ts.map +1 -0
- package/dist/mcp/index.js +2403 -501
- package/dist/mcp.js +8 -4
- package/dist/pr-groups/postgres.d.ts.map +1 -1
- package/dist/project-registration/postgres.d.ts.map +1 -1
- package/dist/project-registration.js +290 -81
- package/dist/registry.d.ts +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +73 -58
- 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 +5161 -8425
- 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 +268 -74
- package/dist/task-manifest/postgres.d.ts.map +1 -1
- package/dist/task-manifest.js +15 -6
- 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 +1878 -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
|
@@ -3692,6 +3692,20 @@ function ensureSchema(db) {
|
|
|
3692
3692
|
ensureIndex("CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_event ON webhook_deliveries(event)");
|
|
3693
3693
|
ensureColumn("task_comments", "type", "TEXT DEFAULT 'comment'");
|
|
3694
3694
|
ensureColumn("task_comments", "progress_pct", "INTEGER");
|
|
3695
|
+
ensureTable("plan_comments", `
|
|
3696
|
+
CREATE TABLE plan_comments (
|
|
3697
|
+
id TEXT PRIMARY KEY,
|
|
3698
|
+
plan_id TEXT NOT NULL REFERENCES plans(id) ON DELETE CASCADE,
|
|
3699
|
+
agent_id TEXT,
|
|
3700
|
+
session_id TEXT,
|
|
3701
|
+
content TEXT NOT NULL,
|
|
3702
|
+
type TEXT DEFAULT 'comment' CHECK(type IN ('comment', 'progress', 'note')),
|
|
3703
|
+
progress_pct INTEGER CHECK(progress_pct IS NULL OR (progress_pct >= 0 AND progress_pct <= 100)),
|
|
3704
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
3705
|
+
machine_id TEXT,
|
|
3706
|
+
synced_at TEXT
|
|
3707
|
+
)`);
|
|
3708
|
+
ensureIndex("CREATE INDEX IF NOT EXISTS idx_plan_comments_plan ON plan_comments(plan_id)");
|
|
3695
3709
|
ensureColumn("projects", "machine_id", "TEXT");
|
|
3696
3710
|
ensureColumn("projects", "synced_at", "TEXT");
|
|
3697
3711
|
ensureColumn("tasks", "machine_id", "TEXT");
|
|
@@ -4673,6 +4687,7 @@ var init_machines = __esm(() => {
|
|
|
4673
4687
|
"task_lists",
|
|
4674
4688
|
"plans",
|
|
4675
4689
|
"task_comments",
|
|
4690
|
+
"plan_comments",
|
|
4676
4691
|
"sessions",
|
|
4677
4692
|
"task_history",
|
|
4678
4693
|
"webhooks",
|
|
@@ -4866,20 +4881,16 @@ var init_identity_mapping = __esm(() => {
|
|
|
4866
4881
|
// src/storage/config.ts
|
|
4867
4882
|
var exports_config = {};
|
|
4868
4883
|
__export(exports_config, {
|
|
4869
|
-
parseStorageMode: () => parseStorageMode,
|
|
4870
4884
|
parseStorageBackend: () => parseStorageBackend,
|
|
4871
4885
|
loadTodosStorageConfig: () => loadTodosStorageConfig,
|
|
4872
4886
|
loadStorageConfig: () => loadStorageConfig,
|
|
4873
4887
|
isTodosShadowEnabled: () => isTodosShadowEnabled,
|
|
4874
|
-
isTodosRemoteStorageEnabled: () => isTodosRemoteStorageEnabled,
|
|
4875
4888
|
isTodosPostgresBackend: () => isTodosPostgresBackend,
|
|
4876
4889
|
getTodosStorageShadowEnvName: () => getTodosStorageShadowEnvName,
|
|
4877
|
-
getTodosStorageMode: () => getTodosStorageMode,
|
|
4878
4890
|
getTodosStorageEnvName: () => getTodosStorageEnvName,
|
|
4879
4891
|
getTodosStorageDatabaseUrl: () => getTodosStorageDatabaseUrl,
|
|
4880
4892
|
getTodosStorageDatabaseEnv: () => getTodosStorageDatabaseEnv,
|
|
4881
4893
|
getTodosStorageBackend: () => getTodosStorageBackend,
|
|
4882
|
-
getStorageMode: () => getStorageMode,
|
|
4883
4894
|
getStorageDatabaseUrl: () => getStorageDatabaseUrl,
|
|
4884
4895
|
getStorageDatabaseEnv: () => getStorageDatabaseEnv,
|
|
4885
4896
|
getCanonicalTodosRdsConfig: () => getCanonicalTodosRdsConfig,
|
|
@@ -4889,7 +4900,6 @@ __export(exports_config, {
|
|
|
4889
4900
|
TODOS_STORAGE_FALLBACK_ENV: () => TODOS_STORAGE_FALLBACK_ENV,
|
|
4890
4901
|
TODOS_STORAGE_ENV: () => TODOS_STORAGE_ENV,
|
|
4891
4902
|
STORAGE_TABLES: () => STORAGE_TABLES,
|
|
4892
|
-
REMOVED_STORAGE_MODE_ENV_KEYS: () => REMOVED_STORAGE_MODE_ENV_KEYS,
|
|
4893
4903
|
CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV: () => CANONICAL_TODOS_RDS_RUNTIME_PATH_ENV,
|
|
4894
4904
|
CANONICAL_TODOS_RDS_DATABASE: () => CANONICAL_TODOS_RDS_DATABASE,
|
|
4895
4905
|
CANONICAL_TODOS_RDS_CLUSTER_ENV: () => CANONICAL_TODOS_RDS_CLUSTER_ENV
|
|
@@ -4944,9 +4954,6 @@ function loadStorageConfig(env = process.env) {
|
|
|
4944
4954
|
function isTodosPostgresBackend(config) {
|
|
4945
4955
|
return config.mode === "postgres";
|
|
4946
4956
|
}
|
|
4947
|
-
function isTodosRemoteStorageEnabled(config) {
|
|
4948
|
-
return isTodosPostgresBackend(config);
|
|
4949
|
-
}
|
|
4950
4957
|
function assertTodosRemoteStorageConfig(config) {
|
|
4951
4958
|
if (!isTodosPostgresBackend(config))
|
|
4952
4959
|
return;
|
|
@@ -4962,32 +4969,15 @@ function parseStorageBackend(value) {
|
|
|
4962
4969
|
return "sqlite";
|
|
4963
4970
|
if (normalized === "postgres" || normalized === "postgresql")
|
|
4964
4971
|
return "postgres";
|
|
4965
|
-
if (["local", "remote", "cloud", "hybrid", "self_hosted"].includes(normalized)) {
|
|
4966
|
-
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.`);
|
|
4967
|
-
}
|
|
4968
4972
|
throw new Error(`Storage backend must be sqlite or postgres`);
|
|
4969
4973
|
}
|
|
4970
|
-
function parseStorageMode(value) {
|
|
4971
|
-
return parseStorageBackend(value);
|
|
4972
|
-
}
|
|
4973
4974
|
function getTodosStorageBackend(env = process.env) {
|
|
4974
|
-
for (const key of REMOVED_STORAGE_MODE_ENV_KEYS) {
|
|
4975
|
-
if (Object.hasOwn(env, key) && env[key] !== undefined) {
|
|
4976
|
-
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.`);
|
|
4977
|
-
}
|
|
4978
|
-
}
|
|
4979
4975
|
if (!getTodosStorageDatabaseUrl(env))
|
|
4980
4976
|
return "sqlite";
|
|
4981
4977
|
if (isTodosShadowEnabled(env))
|
|
4982
4978
|
return "sqlite";
|
|
4983
4979
|
return "postgres";
|
|
4984
4980
|
}
|
|
4985
|
-
function getTodosStorageMode(env = process.env) {
|
|
4986
|
-
return getTodosStorageBackend(env);
|
|
4987
|
-
}
|
|
4988
|
-
function getStorageMode(env = process.env) {
|
|
4989
|
-
return getTodosStorageBackend(env);
|
|
4990
|
-
}
|
|
4991
4981
|
function isTodosShadowEnabled(env = process.env) {
|
|
4992
4982
|
return parseBoolean(readStorageEnv(env, "shadow").value, false);
|
|
4993
4983
|
}
|
|
@@ -5054,7 +5044,7 @@ function parsePositiveInteger(value, fallback) {
|
|
|
5054
5044
|
}
|
|
5055
5045
|
return parsed;
|
|
5056
5046
|
}
|
|
5057
|
-
var TODOS_STORAGE_TABLES, STORAGE_TABLES, TODOS_STORAGE_ENV, TODOS_STORAGE_FALLBACK_ENV,
|
|
5047
|
+
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";
|
|
5058
5048
|
var init_config2 = __esm(() => {
|
|
5059
5049
|
TODOS_STORAGE_TABLES = [
|
|
5060
5050
|
"todos_sync_records",
|
|
@@ -5093,12 +5083,6 @@ var init_config2 = __esm(() => {
|
|
|
5093
5083
|
syncBatchSize: "TODOS_SYNC_BATCH_SIZE",
|
|
5094
5084
|
syncDryRun: "TODOS_SYNC_DRY_RUN"
|
|
5095
5085
|
};
|
|
5096
|
-
REMOVED_STORAGE_MODE_ENV_KEYS = [
|
|
5097
|
-
"HASNA_TODOS_STORAGE_MODE",
|
|
5098
|
-
"HASNA_TODOS_MODE",
|
|
5099
|
-
"TODOS_STORAGE_MODE",
|
|
5100
|
-
"TODOS_MODE"
|
|
5101
|
-
];
|
|
5102
5086
|
});
|
|
5103
5087
|
|
|
5104
5088
|
// src/storage/shadow-outbox-schema.ts
|
|
@@ -7270,22 +7254,31 @@ function updateTaskList2(id, input, db) {
|
|
|
7270
7254
|
throw new TaskListNotFoundError(id);
|
|
7271
7255
|
const sets = ["updated_at = ?"];
|
|
7272
7256
|
const params = [now()];
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
7257
|
+
const projectId = input.project_id === "" || input.project_id === null ? null : input.project_id === undefined ? existing.project_id : input.project_id;
|
|
7258
|
+
const scopeChanged = projectId !== existing.project_id;
|
|
7259
|
+
const effectiveSlug = input.slug !== undefined ? slugify(input.slug) : existing.slug;
|
|
7260
|
+
if (input.slug !== undefined && !effectiveSlug) {
|
|
7261
|
+
throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
|
|
7262
|
+
}
|
|
7263
|
+
if (scopeChanged || input.slug !== undefined && effectiveSlug !== existing.slug) {
|
|
7264
|
+
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);
|
|
7278
7265
|
if (duplicate) {
|
|
7279
|
-
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${
|
|
7266
|
+
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${effectiveSlug}" already exists in this scope`);
|
|
7280
7267
|
}
|
|
7281
|
-
if (
|
|
7268
|
+
if (scopeChanged || effectiveSlug !== existing.slug) {
|
|
7282
7269
|
releaseCanonicalSlugClaims("task_list", id, d);
|
|
7283
|
-
if (!claimCanonicalSlug("task_list", taskListSlugScopeKey(
|
|
7284
|
-
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${
|
|
7270
|
+
if (!claimCanonicalSlug("task_list", taskListSlugScopeKey(projectId), effectiveSlug, id, d)) {
|
|
7271
|
+
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${effectiveSlug}" already exists in this scope`);
|
|
7285
7272
|
}
|
|
7286
7273
|
}
|
|
7274
|
+
}
|
|
7275
|
+
if (input.slug !== undefined) {
|
|
7287
7276
|
sets.push("slug = ?");
|
|
7288
|
-
params.push(
|
|
7277
|
+
params.push(effectiveSlug);
|
|
7278
|
+
}
|
|
7279
|
+
if (projectId !== existing.project_id) {
|
|
7280
|
+
sets.push("project_id = ?");
|
|
7281
|
+
params.push(projectId);
|
|
7289
7282
|
}
|
|
7290
7283
|
if (input.name !== undefined) {
|
|
7291
7284
|
sets.push("name = ?");
|
|
@@ -8794,7 +8787,7 @@ function getTaskGraph(taskId, direction = "both", db) {
|
|
|
8794
8787
|
const deps = getTaskDependencies(t.id, d);
|
|
8795
8788
|
const hasUnfinishedDeps = deps.some((dep) => {
|
|
8796
8789
|
const depTask = getTask(dep.depends_on, d);
|
|
8797
|
-
return depTask && depTask.status
|
|
8790
|
+
return depTask && isBlockingDependencyStatus(depTask.status);
|
|
8798
8791
|
});
|
|
8799
8792
|
return { id: t.id, short_id: t.short_id, title: t.title, status: t.status, priority: t.priority, is_blocked: hasUnfinishedDeps };
|
|
8800
8793
|
}
|
|
@@ -8918,7 +8911,7 @@ function getBlockingDeps(id, db) {
|
|
|
8918
8911
|
const blocking = [];
|
|
8919
8912
|
for (const dep of deps) {
|
|
8920
8913
|
const task = getTask(dep.depends_on, d);
|
|
8921
|
-
if (task && task.status
|
|
8914
|
+
if (task && isBlockingDependencyStatus(task.status))
|
|
8922
8915
|
blocking.push(task);
|
|
8923
8916
|
}
|
|
8924
8917
|
return blocking;
|
|
@@ -9242,7 +9235,7 @@ function getNextTask2(agentId, filters, db) {
|
|
|
9242
9235
|
conditions.push(`id IN (SELECT task_id FROM task_tags WHERE tag IN (${placeholders}))`);
|
|
9243
9236
|
params.push(...filters.tags);
|
|
9244
9237
|
}
|
|
9245
|
-
conditions.push("id NOT IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status
|
|
9238
|
+
conditions.push("id NOT IN (SELECT td.task_id FROM task_dependencies td JOIN tasks dep ON dep.id = td.depends_on WHERE dep.status NOT IN ('completed', 'cancelled'))");
|
|
9246
9239
|
const where = conditions.join(" AND ");
|
|
9247
9240
|
let recentProjectIds = [];
|
|
9248
9241
|
const assignedAliasParams = [];
|
|
@@ -9286,7 +9279,7 @@ function getActiveWork2(filters, db) {
|
|
|
9286
9279
|
}
|
|
9287
9280
|
function getTasksChangedSince(since, filters, db) {
|
|
9288
9281
|
const d = db || getDatabase();
|
|
9289
|
-
const conditions = ["updated_at > ?"];
|
|
9282
|
+
const conditions = ["(julianday(updated_at) IS NULL OR julianday(updated_at) > julianday(?))"];
|
|
9290
9283
|
const params = [since];
|
|
9291
9284
|
if (filters?.project_id) {
|
|
9292
9285
|
conditions.push("project_id = ?");
|
|
@@ -9321,8 +9314,8 @@ function failTask2(id, agentId, reason, options, db) {
|
|
|
9321
9314
|
const safeMeta = sanitizePreWriteValue(meta, "task.failure.metadata");
|
|
9322
9315
|
const timestamp2 = now();
|
|
9323
9316
|
const failTx = d.transaction(() => {
|
|
9324
|
-
const res = d.run(`UPDATE tasks SET status = 'failed', reason = ?, locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
|
|
9325
|
-
WHERE id = ? AND version = ?`, [safeReason, JSON.stringify(safeMeta), timestamp2, id, task.version]);
|
|
9317
|
+
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 = ?
|
|
9318
|
+
WHERE id = ? AND version = ?`, [safeReason, timestamp2, JSON.stringify(safeMeta), timestamp2, id, task.version]);
|
|
9326
9319
|
if (res.changes === 0) {
|
|
9327
9320
|
const current = getTask(id, d);
|
|
9328
9321
|
throw new VersionConflictError(id, task.version, current?.version ?? -1);
|
|
@@ -9334,6 +9327,7 @@ function failTask2(id, agentId, reason, options, db) {
|
|
|
9334
9327
|
status: "failed",
|
|
9335
9328
|
locked_by: null,
|
|
9336
9329
|
locked_at: null,
|
|
9330
|
+
completed_at: task.completed_at ?? timestamp2,
|
|
9337
9331
|
reason: safeReason,
|
|
9338
9332
|
metadata: safeMeta,
|
|
9339
9333
|
version: task.version + 1,
|
|
@@ -9982,13 +9976,20 @@ function updateTaskStored(id, input, db) {
|
|
|
9982
9976
|
if (isTerminalStatus(input.status)) {
|
|
9983
9977
|
sets.push("locked_by = NULL");
|
|
9984
9978
|
sets.push("locked_at = NULL");
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
|
|
9988
|
-
|
|
9979
|
+
if (input.status === "completed") {
|
|
9980
|
+
sets.push("completed_at = ?");
|
|
9981
|
+
params.push(completionTimestamp);
|
|
9982
|
+
} else {
|
|
9983
|
+
sets.push("completed_at = COALESCE(completed_at, ?)");
|
|
9984
|
+
params.push(completionTimestamp);
|
|
9985
|
+
}
|
|
9989
9986
|
} else if (task.status === "completed" && input.completed_at === undefined) {
|
|
9990
9987
|
sets.push("completed_at = NULL");
|
|
9991
9988
|
}
|
|
9989
|
+
if (input.status === "in_progress") {
|
|
9990
|
+
sets.push("started_at = COALESCE(started_at, ?)");
|
|
9991
|
+
params.push(timestamp2);
|
|
9992
|
+
}
|
|
9992
9993
|
}
|
|
9993
9994
|
if (input.priority !== undefined) {
|
|
9994
9995
|
sets.push("priority = ?");
|
|
@@ -10126,7 +10127,6 @@ function updateTaskStored(id, input, db) {
|
|
|
10126
10127
|
if (input.approved_by !== undefined)
|
|
10127
10128
|
logTaskChange2(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
10128
10129
|
const reopened = input.status !== undefined && input.status !== "completed" && task.status === "completed" && input.completed_at === undefined;
|
|
10129
|
-
const completedNow = input.status === "completed";
|
|
10130
10130
|
const terminalNow = input.status !== undefined && isTerminalStatus(input.status);
|
|
10131
10131
|
const updatedTask = {
|
|
10132
10132
|
...task,
|
|
@@ -10137,7 +10137,8 @@ function updateTaskStored(id, input, db) {
|
|
|
10137
10137
|
updated_at: timestamp2,
|
|
10138
10138
|
locked_by: terminalNow ? null : task.locked_by,
|
|
10139
10139
|
locked_at: terminalNow ? null : task.locked_at,
|
|
10140
|
-
completed_at:
|
|
10140
|
+
completed_at: input.status === "completed" ? completionTimestamp : input.completed_at !== undefined ? input.completed_at : terminalNow ? task.completed_at ?? completionTimestamp : reopened ? null : task.completed_at,
|
|
10141
|
+
started_at: input.status === "in_progress" ? task.started_at ?? timestamp2 : task.started_at,
|
|
10141
10142
|
sla_minutes: input.sla_minutes !== undefined ? input.sla_minutes : task.sla_minutes,
|
|
10142
10143
|
actual_minutes: input.actual_minutes ?? task.actual_minutes,
|
|
10143
10144
|
confidence: input.confidence !== undefined ? input.confidence : task.confidence,
|
|
@@ -10198,6 +10199,9 @@ function deleteTask(id, db) {
|
|
|
10198
10199
|
version: row.version
|
|
10199
10200
|
}, d);
|
|
10200
10201
|
const result = d.run("DELETE FROM tasks WHERE id = ?", [id]);
|
|
10202
|
+
if (result.changes > 0) {
|
|
10203
|
+
d.run("DELETE FROM task_dependencies WHERE task_id = ? OR depends_on = ?", [id, id]);
|
|
10204
|
+
}
|
|
10201
10205
|
return result.changes > 0;
|
|
10202
10206
|
}
|
|
10203
10207
|
var init_task_crud = __esm(() => {
|
|
@@ -12808,7 +12812,7 @@ import { createHash as createHash7 } from "crypto";
|
|
|
12808
12812
|
// package.json
|
|
12809
12813
|
var package_default = {
|
|
12810
12814
|
name: "@hasna/todos",
|
|
12811
|
-
version: "0.15.
|
|
12815
|
+
version: "0.15.46",
|
|
12812
12816
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
12813
12817
|
type: "module",
|
|
12814
12818
|
main: "dist/index.js",
|
|
@@ -12854,6 +12858,10 @@ var package_default = {
|
|
|
12854
12858
|
"./task-manifest": {
|
|
12855
12859
|
types: "./dist/task-manifest.d.ts",
|
|
12856
12860
|
import: "./dist/task-manifest.js"
|
|
12861
|
+
},
|
|
12862
|
+
"./task-subtree-transfer": {
|
|
12863
|
+
types: "./dist/task-subtree-transfer.d.ts",
|
|
12864
|
+
import: "./dist/task-subtree-transfer.js"
|
|
12857
12865
|
}
|
|
12858
12866
|
},
|
|
12859
12867
|
workspaces: [
|
|
@@ -12867,8 +12875,8 @@ var package_default = {
|
|
|
12867
12875
|
"README.md"
|
|
12868
12876
|
],
|
|
12869
12877
|
scripts: {
|
|
12870
|
-
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",
|
|
12871
|
-
"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/*'",
|
|
12878
|
+
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",
|
|
12879
|
+
"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/*'",
|
|
12872
12880
|
migrate: "bun run src/server/index.ts migrate",
|
|
12873
12881
|
"backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
|
|
12874
12882
|
"generate:sdk": "bun run scripts/generate-sdk.ts",
|
|
@@ -12922,7 +12930,7 @@ var package_default = {
|
|
|
12922
12930
|
author: "Andrei Hasna <andrei@hasna.com>",
|
|
12923
12931
|
license: "Apache-2.0",
|
|
12924
12932
|
dependencies: {
|
|
12925
|
-
"@hasna/contracts": "0.
|
|
12933
|
+
"@hasna/contracts": "0.13.4",
|
|
12926
12934
|
"@hasna/events": "^0.1.11",
|
|
12927
12935
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
12928
12936
|
chalk: "^5.4.1",
|
|
@@ -12960,6 +12968,57 @@ function normalizeAgentNameInput(name) {
|
|
|
12960
12968
|
// src/storage/postgres-adapter.ts
|
|
12961
12969
|
init_creator_identity();
|
|
12962
12970
|
|
|
12971
|
+
// src/lib/instant-compare.ts
|
|
12972
|
+
var SQLITE_STAMP = /^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?(?:Z|([+-])(\d{2}):(\d{2}))?)?$/;
|
|
12973
|
+
var DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
12974
|
+
function isLeapYear(year) {
|
|
12975
|
+
return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0;
|
|
12976
|
+
}
|
|
12977
|
+
function sqliteJulianDay(value) {
|
|
12978
|
+
const m = SQLITE_STAMP.exec(value);
|
|
12979
|
+
if (!m)
|
|
12980
|
+
return null;
|
|
12981
|
+
const year = Number(m[1]);
|
|
12982
|
+
const month = Number(m[2]);
|
|
12983
|
+
const day = Number(m[3]);
|
|
12984
|
+
const hour = m[4] === undefined ? 0 : Number(m[4]);
|
|
12985
|
+
const minute = m[5] === undefined ? 0 : Number(m[5]);
|
|
12986
|
+
const second = m[6] === undefined ? 0 : Number(m[6]);
|
|
12987
|
+
const frac = m[7];
|
|
12988
|
+
const sign = m[8];
|
|
12989
|
+
const offsetHour = m[9];
|
|
12990
|
+
const offsetMinute = m[10];
|
|
12991
|
+
if (month < 1 || month > 12)
|
|
12992
|
+
return null;
|
|
12993
|
+
if (hour > 23 || minute > 59 || second > 59)
|
|
12994
|
+
return null;
|
|
12995
|
+
const maxDay = (DAYS_IN_MONTH[month - 1] ?? 0) + (month === 2 && isLeapYear(year) ? 1 : 0);
|
|
12996
|
+
if (day < 1 || day > maxDay)
|
|
12997
|
+
return null;
|
|
12998
|
+
let offsetMinutes = 0;
|
|
12999
|
+
if (sign !== undefined) {
|
|
13000
|
+
const oh = Number(offsetHour ?? "0");
|
|
13001
|
+
const om = Number(offsetMinute ?? "0");
|
|
13002
|
+
if (oh > 23 || om > 59)
|
|
13003
|
+
return null;
|
|
13004
|
+
offsetMinutes = oh * 60 + om;
|
|
13005
|
+
if (sign === "-")
|
|
13006
|
+
offsetMinutes = -offsetMinutes;
|
|
13007
|
+
}
|
|
13008
|
+
const micros = frac === undefined ? 0 : Number((frac + "000000").slice(0, 6));
|
|
13009
|
+
const ms = Date.UTC(year, month - 1, day, hour, minute, second);
|
|
13010
|
+
return (ms + micros / 1000) / 86400000 + 2440587.5 - offsetMinutes / 1440;
|
|
13011
|
+
}
|
|
13012
|
+
function changedSinceStampNewer(stamp, since) {
|
|
13013
|
+
const stampJd = sqliteJulianDay(stamp);
|
|
13014
|
+
if (stampJd === null)
|
|
13015
|
+
return true;
|
|
13016
|
+
const sinceJd = sqliteJulianDay(since);
|
|
13017
|
+
if (sinceJd === null)
|
|
13018
|
+
return false;
|
|
13019
|
+
return stampJd > sinceJd;
|
|
13020
|
+
}
|
|
13021
|
+
|
|
12963
13022
|
// src/lib/plan-project-link-contract.ts
|
|
12964
13023
|
init_types();
|
|
12965
13024
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -13783,7 +13842,26 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
13783
13842
|
list: async (projectId) => (await store.list("plans")).filter((plan) => projectId === undefined || plan.project_id === projectId).sort((a, b) => a.name.localeCompare(b.name)),
|
|
13784
13843
|
update: (id, input) => updatePlan(id, input, store),
|
|
13785
13844
|
completeAtRevision: (id, expectedUpdatedAt, context) => store.completePlanAtRevision(id, expectedUpdatedAt, context),
|
|
13786
|
-
delete: (id, context) => store.deletePlan(id, context)
|
|
13845
|
+
delete: (id, context) => store.deletePlan(id, context),
|
|
13846
|
+
addComment: (input, context) => addPlanComment(input, store, context),
|
|
13847
|
+
getComments: async (planId) => {
|
|
13848
|
+
const pages = [];
|
|
13849
|
+
let before;
|
|
13850
|
+
while (true) {
|
|
13851
|
+
const page = await store.listPlanComments(planId, { limit: 1000, ...before ? { before } : {} });
|
|
13852
|
+
if (page.length === 0)
|
|
13853
|
+
break;
|
|
13854
|
+
pages.unshift(page);
|
|
13855
|
+
if (page.length < 1000)
|
|
13856
|
+
break;
|
|
13857
|
+
const oldest = page[0];
|
|
13858
|
+
before = { created_at: oldest.created_at, id: oldest.id };
|
|
13859
|
+
}
|
|
13860
|
+
return pages.flat().map(redactPlanComment).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
|
|
13861
|
+
},
|
|
13862
|
+
getCommentsPage: async (planId, options2) => {
|
|
13863
|
+
return (await store.listPlanComments(planId, options2)).map(redactPlanComment).sort((a, b) => a.created_at.localeCompare(b.created_at) || a.id.localeCompare(b.id));
|
|
13864
|
+
}
|
|
13787
13865
|
},
|
|
13788
13866
|
planProjectLinks: {
|
|
13789
13867
|
apply: (input, context) => store.applyPlanProjectLink(input, context),
|
|
@@ -13877,11 +13955,16 @@ class PostgresJsonRecordStore {
|
|
|
13877
13955
|
return context?.requestId ?? this.sourceMachineId ?? null;
|
|
13878
13956
|
}
|
|
13879
13957
|
async ensureSchema() {
|
|
13880
|
-
this.schemaReady
|
|
13881
|
-
|
|
13882
|
-
|
|
13883
|
-
|
|
13884
|
-
|
|
13958
|
+
if (!this.schemaReady) {
|
|
13959
|
+
this.schemaReady = (async () => {
|
|
13960
|
+
for (const sql of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
|
|
13961
|
+
await this.options.client.query(sql);
|
|
13962
|
+
}
|
|
13963
|
+
})().catch((error) => {
|
|
13964
|
+
this.schemaReady = null;
|
|
13965
|
+
throw error;
|
|
13966
|
+
});
|
|
13967
|
+
}
|
|
13885
13968
|
await this.schemaReady;
|
|
13886
13969
|
}
|
|
13887
13970
|
async get(type, id) {
|
|
@@ -14001,6 +14084,27 @@ class PostgresJsonRecordStore {
|
|
|
14001
14084
|
LIMIT $${params.length}`, params);
|
|
14002
14085
|
return result.rows.map((row) => payloadRecord2(row.payload)).reverse();
|
|
14003
14086
|
}
|
|
14087
|
+
async listPlanComments(planId, options = {}) {
|
|
14088
|
+
await this.ensureSchema();
|
|
14089
|
+
const limit = options.limit ?? 100;
|
|
14090
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 1001) {
|
|
14091
|
+
throw new Error("Postgres plan comment limit must be an integer between 1 and 1001");
|
|
14092
|
+
}
|
|
14093
|
+
const params = [this.service, planId];
|
|
14094
|
+
let cursorPredicate = "";
|
|
14095
|
+
if (options.before) {
|
|
14096
|
+
params.push(options.before.created_at, options.before.id);
|
|
14097
|
+
cursorPredicate = `AND (payload->>'created_at', object_id) < ($3, $4)`;
|
|
14098
|
+
}
|
|
14099
|
+
params.push(limit);
|
|
14100
|
+
const result = await this.options.client.query(`/* todos:list-plan-comments */ SELECT payload FROM ${this.tableName}
|
|
14101
|
+
WHERE service = $1 AND object_type = 'plan_comments' AND deleted_at IS NULL
|
|
14102
|
+
AND payload->>'plan_id' = $2
|
|
14103
|
+
${cursorPredicate}
|
|
14104
|
+
ORDER BY payload->>'created_at' DESC, object_id DESC
|
|
14105
|
+
LIMIT $${params.length}`, params);
|
|
14106
|
+
return result.rows.map((row) => payloadRecord2(row.payload)).reverse();
|
|
14107
|
+
}
|
|
14004
14108
|
async listRecords(type) {
|
|
14005
14109
|
await this.ensureSchema();
|
|
14006
14110
|
const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at
|
|
@@ -15162,6 +15266,15 @@ class PostgresJsonRecordStore {
|
|
|
15162
15266
|
WHERE r.service = $1 AND r.object_type = 'tasks' AND r.deleted_at IS NULL
|
|
15163
15267
|
AND r.payload->>'plan_id' = $2 AND EXISTS (SELECT 1 FROM target)
|
|
15164
15268
|
RETURNING 1
|
|
15269
|
+
), deleted_plan_comments AS (
|
|
15270
|
+
UPDATE ${this.tableName} r SET
|
|
15271
|
+
deleted_at = $3::timestamptz,
|
|
15272
|
+
updated_at = $3::timestamptz,
|
|
15273
|
+
source_machine_id = COALESCE($4, r.source_machine_id),
|
|
15274
|
+
version = COALESCE(r.version, 0) + 1
|
|
15275
|
+
WHERE r.service = $1 AND r.object_type = 'plan_comments' AND r.deleted_at IS NULL
|
|
15276
|
+
AND r.payload->>'plan_id' = $2 AND EXISTS (SELECT 1 FROM target)
|
|
15277
|
+
RETURNING 1
|
|
15165
15278
|
), deleted_plan AS (
|
|
15166
15279
|
UPDATE ${this.tableName} r SET
|
|
15167
15280
|
deleted_at = $3::timestamptz,
|
|
@@ -15337,7 +15450,8 @@ async function updateTask(id, input, store, context) {
|
|
|
15337
15450
|
task_list_id: input.task_list_id !== undefined ? input.task_list_id : existing.task_list_id,
|
|
15338
15451
|
parent_id: input.parent_id !== undefined ? input.parent_id : existing.parent_id,
|
|
15339
15452
|
created_by: existing.created_by,
|
|
15340
|
-
completed_at:
|
|
15453
|
+
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,
|
|
15454
|
+
started_at: input.status === "in_progress" ? existing.started_at ?? new Date().toISOString() : existing.started_at
|
|
15341
15455
|
};
|
|
15342
15456
|
const storedTask = await store.upsertTaskWithPlanMembershipGuard(task, [existing.plan_id, effectivePlanId].filter((planId) => Boolean(planId)), input.project_id !== undefined, context, {
|
|
15343
15457
|
operation: "update",
|
|
@@ -15382,7 +15496,8 @@ async function failTask(id, agentId, reason, options, store) {
|
|
|
15382
15496
|
status: "failed",
|
|
15383
15497
|
assigned_to: task.assigned_to ?? agentId ?? null,
|
|
15384
15498
|
reason: reason ?? task.reason,
|
|
15385
|
-
retry_after: options?.retry_after ?? task.retry_after
|
|
15499
|
+
retry_after: options?.retry_after ?? task.retry_after,
|
|
15500
|
+
completed_at: task.completed_at ?? new Date().toISOString()
|
|
15386
15501
|
}, store);
|
|
15387
15502
|
if (!options?.retry)
|
|
15388
15503
|
return { task: failed };
|
|
@@ -15614,7 +15729,7 @@ async function getActiveWork(filters, store) {
|
|
|
15614
15729
|
}));
|
|
15615
15730
|
}
|
|
15616
15731
|
async function getChangedSince(since, filters, store) {
|
|
15617
|
-
return (await listTasks(filters ?? {}, store)).filter((task) => task.updated_at
|
|
15732
|
+
return (await listTasks(filters ?? {}, store)).filter((task) => changedSinceStampNewer(task.updated_at ?? "", since));
|
|
15618
15733
|
}
|
|
15619
15734
|
async function createProject(input, store, context) {
|
|
15620
15735
|
const timestamp = new Date().toISOString();
|
|
@@ -15830,16 +15945,22 @@ async function createTaskList(input, store, context) {
|
|
|
15830
15945
|
async function updateTaskList(id, input, store) {
|
|
15831
15946
|
const list = await requireRecord("task_lists", id, store);
|
|
15832
15947
|
const patch = definedPatch(input);
|
|
15948
|
+
const projectId = input.project_id === "" || input.project_id === null ? null : input.project_id === undefined ? list.project_id : input.project_id;
|
|
15833
15949
|
if (input.slug !== undefined) {
|
|
15834
15950
|
const slug = slugifyRaw(input.slug);
|
|
15835
15951
|
if (!slug)
|
|
15836
15952
|
throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
|
|
15837
|
-
|
|
15953
|
+
patch.slug = slug;
|
|
15954
|
+
}
|
|
15955
|
+
const effectiveSlug = patch.slug ?? list.slug;
|
|
15956
|
+
if (projectId !== list.project_id || effectiveSlug !== list.slug) {
|
|
15957
|
+
const duplicate = (await store.list("task_lists")).find((candidate) => candidate.id !== id && candidate.project_id === projectId && candidate.slug === effectiveSlug);
|
|
15838
15958
|
if (duplicate) {
|
|
15839
|
-
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${
|
|
15959
|
+
throw new ResourceConflictError("TASK_LIST_SLUG_CONFLICT", `Task list with slug "${effectiveSlug}" already exists in this scope`);
|
|
15840
15960
|
}
|
|
15841
|
-
patch.slug = slug;
|
|
15842
15961
|
}
|
|
15962
|
+
if (projectId !== list.project_id)
|
|
15963
|
+
patch.project_id = projectId;
|
|
15843
15964
|
return store.upsert("task_lists", {
|
|
15844
15965
|
...list,
|
|
15845
15966
|
...patch,
|
|
@@ -15932,6 +16053,22 @@ async function addComment(input, store, context) {
|
|
|
15932
16053
|
function redactComment(comment) {
|
|
15933
16054
|
return { ...comment, content: redactEvidenceText(comment.content) };
|
|
15934
16055
|
}
|
|
16056
|
+
async function addPlanComment(input, store, context) {
|
|
16057
|
+
const comment = {
|
|
16058
|
+
id: randomUUID(),
|
|
16059
|
+
plan_id: input.plan_id,
|
|
16060
|
+
agent_id: input.agent_id ?? context?.agentId ?? null,
|
|
16061
|
+
session_id: input.session_id ?? context?.sessionId ?? null,
|
|
16062
|
+
content: redactEvidenceText(input.content),
|
|
16063
|
+
type: input.type ?? "comment",
|
|
16064
|
+
progress_pct: input.progress_pct ?? null,
|
|
16065
|
+
created_at: new Date().toISOString()
|
|
16066
|
+
};
|
|
16067
|
+
return store.upsert("plan_comments", comment, context);
|
|
16068
|
+
}
|
|
16069
|
+
function redactPlanComment(comment) {
|
|
16070
|
+
return { ...comment, content: redactEvidenceText(comment.content) };
|
|
16071
|
+
}
|
|
15935
16072
|
async function exportSnapshot(store) {
|
|
15936
16073
|
return {
|
|
15937
16074
|
exportedAt: new Date().toISOString(),
|
|
@@ -16523,14 +16660,23 @@ class PostgresTodosProjectRegistrationBackend {
|
|
|
16523
16660
|
this.cursorTableName = safeIdentifier(options.cursorTableName ?? "todos_sync_cursors", "cursorTableName");
|
|
16524
16661
|
}
|
|
16525
16662
|
async ensureSchema() {
|
|
16526
|
-
this.schemaReady
|
|
16527
|
-
|
|
16528
|
-
|
|
16529
|
-
|
|
16530
|
-
|
|
16531
|
-
|
|
16663
|
+
if (this.schemaReady === null) {
|
|
16664
|
+
const attempt = (async () => {
|
|
16665
|
+
for (const statement of postgresTodosSyncSchemaSql(this.tableName, this.cursorTableName)) {
|
|
16666
|
+
await this.client.query(statement);
|
|
16667
|
+
}
|
|
16668
|
+
for (const statement of postgresTodosProjectRegistrationSchemaSql()) {
|
|
16669
|
+
await this.client.query(statement);
|
|
16670
|
+
}
|
|
16671
|
+
})();
|
|
16672
|
+
this.schemaReady = attempt;
|
|
16673
|
+
try {
|
|
16674
|
+
await attempt;
|
|
16675
|
+
} catch (error) {
|
|
16676
|
+
this.schemaReady = null;
|
|
16677
|
+
throw error;
|
|
16532
16678
|
}
|
|
16533
|
-
}
|
|
16679
|
+
}
|
|
16534
16680
|
await this.schemaReady;
|
|
16535
16681
|
}
|
|
16536
16682
|
async transaction(fn) {
|
|
@@ -16817,6 +16963,40 @@ init_tasks();
|
|
|
16817
16963
|
init_projects();
|
|
16818
16964
|
init_plans();
|
|
16819
16965
|
|
|
16966
|
+
// src/db/plan-comments.ts
|
|
16967
|
+
init_types();
|
|
16968
|
+
init_database();
|
|
16969
|
+
init_plans();
|
|
16970
|
+
init_prewrite_secrets();
|
|
16971
|
+
function addPlanComment2(input, db) {
|
|
16972
|
+
const d = db || getDatabase();
|
|
16973
|
+
if (!getPlan(input.plan_id, d)) {
|
|
16974
|
+
throw new PlanNotFoundError(input.plan_id);
|
|
16975
|
+
}
|
|
16976
|
+
const id = uuid();
|
|
16977
|
+
const timestamp2 = now();
|
|
16978
|
+
d.run(`INSERT INTO plan_comments (id, plan_id, agent_id, session_id, content, type, progress_pct, created_at)
|
|
16979
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
16980
|
+
id,
|
|
16981
|
+
input.plan_id,
|
|
16982
|
+
input.agent_id || null,
|
|
16983
|
+
input.session_id || null,
|
|
16984
|
+
sanitizePreWriteText(input.content, "comment.content"),
|
|
16985
|
+
input.type || "comment",
|
|
16986
|
+
input.progress_pct ?? null,
|
|
16987
|
+
timestamp2
|
|
16988
|
+
]);
|
|
16989
|
+
return getPlanComment(id, d);
|
|
16990
|
+
}
|
|
16991
|
+
function getPlanComment(id, db) {
|
|
16992
|
+
const d = db || getDatabase();
|
|
16993
|
+
return d.query("SELECT * FROM plan_comments WHERE id = ?").get(id);
|
|
16994
|
+
}
|
|
16995
|
+
function listPlanComments(planId, db) {
|
|
16996
|
+
const d = db || getDatabase();
|
|
16997
|
+
return d.query("SELECT * FROM plan_comments WHERE plan_id = ? ORDER BY created_at, rowid").all(planId);
|
|
16998
|
+
}
|
|
16999
|
+
|
|
16820
17000
|
// src/db/agents.ts
|
|
16821
17001
|
init_types();
|
|
16822
17002
|
init_database();
|
|
@@ -17469,6 +17649,20 @@ init_task_lists();
|
|
|
17469
17649
|
init_templates();
|
|
17470
17650
|
init_audit();
|
|
17471
17651
|
init_comments();
|
|
17652
|
+
|
|
17653
|
+
// src/lib/comment-cursor.ts
|
|
17654
|
+
function isStrictlyOlder(comment, before) {
|
|
17655
|
+
return comment.created_at < before.created_at || comment.created_at === before.created_at && comment.id < before.id;
|
|
17656
|
+
}
|
|
17657
|
+
function compareCommentKeyset(left, right) {
|
|
17658
|
+
if (isStrictlyOlder(left, right))
|
|
17659
|
+
return -1;
|
|
17660
|
+
if (isStrictlyOlder(right, left))
|
|
17661
|
+
return 1;
|
|
17662
|
+
return 0;
|
|
17663
|
+
}
|
|
17664
|
+
|
|
17665
|
+
// src/storage/local-sqlite.ts
|
|
17472
17666
|
init_database();
|
|
17473
17667
|
|
|
17474
17668
|
// src/db/integrity.ts
|
|
@@ -18164,7 +18358,7 @@ function matchesExtraFilters(task, filter) {
|
|
|
18164
18358
|
}
|
|
18165
18359
|
if (filter.tags?.length) {
|
|
18166
18360
|
const taskTags = new Set(task.tags ?? []);
|
|
18167
|
-
if (!filter.tags.
|
|
18361
|
+
if (!filter.tags.some((tag) => taskTags.has(tag)))
|
|
18168
18362
|
return false;
|
|
18169
18363
|
}
|
|
18170
18364
|
if (filter.include_subtasks !== true && filter.parent_id === undefined && task.parent_id)
|
|
@@ -18240,7 +18434,22 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
18240
18434
|
list: (projectId) => listPlans(projectId, database()),
|
|
18241
18435
|
update: (id, input) => updatePlan2(id, input, database()),
|
|
18242
18436
|
completeAtRevision: (id, expectedUpdatedAt) => completePlanAtRevision(id, expectedUpdatedAt, database()),
|
|
18243
|
-
delete: (id) => deletePlan(id, database())
|
|
18437
|
+
delete: (id) => deletePlan(id, database()),
|
|
18438
|
+
addComment: (input) => addPlanComment2(input, database()),
|
|
18439
|
+
getComments: (planId) => listPlanComments(planId, database()),
|
|
18440
|
+
getCommentsPage: (planId, options2) => {
|
|
18441
|
+
if (options2?.limit !== undefined && (!Number.isSafeInteger(options2.limit) || options2.limit < 1 || options2.limit > 1001)) {
|
|
18442
|
+
throw new Error("Plan comment limit must be an integer between 1 and 1001");
|
|
18443
|
+
}
|
|
18444
|
+
let comments = listPlanComments(planId, database());
|
|
18445
|
+
comments = comments.sort(compareCommentKeyset);
|
|
18446
|
+
if (options2?.before) {
|
|
18447
|
+
const before = options2.before;
|
|
18448
|
+
comments = comments.filter((comment) => isStrictlyOlder(comment, before));
|
|
18449
|
+
}
|
|
18450
|
+
const limit = options2?.limit ?? 100;
|
|
18451
|
+
return comments.slice(-limit);
|
|
18452
|
+
}
|
|
18244
18453
|
},
|
|
18245
18454
|
planProjectLinks: {
|
|
18246
18455
|
apply: (input) => applyPlanProjectLinkSqlite(input, database()),
|
|
@@ -18281,10 +18490,10 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
18281
18490
|
throw new Error("Comment limit must be an integer between 1 and 1001");
|
|
18282
18491
|
}
|
|
18283
18492
|
let comments = listComments(taskId, database());
|
|
18284
|
-
comments = comments.sort(
|
|
18493
|
+
comments = comments.sort(compareCommentKeyset);
|
|
18285
18494
|
if (options2?.before) {
|
|
18286
18495
|
const before = options2.before;
|
|
18287
|
-
comments = comments.filter((comment) => comment
|
|
18496
|
+
comments = comments.filter((comment) => isStrictlyOlder(comment, before));
|
|
18288
18497
|
}
|
|
18289
18498
|
if (options2?.limit !== undefined)
|
|
18290
18499
|
comments = comments.slice(-options2.limit);
|