@hasna/todos 0.13.2 → 0.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -14
- package/dist/cli/cloud-router.d.ts +57 -7
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/query-commands.d.ts.map +1 -1
- package/dist/cli/doctor-integrity.d.ts +106 -0
- package/dist/cli/doctor-integrity.d.ts.map +1 -0
- package/dist/cli/helpers.d.ts +2 -2
- package/dist/cli/index.js +1461 -778
- package/dist/cli/stage-a.d.ts.map +1 -1
- package/dist/contracts.js +118 -30
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/findings.d.ts +1 -0
- package/dist/db/findings.d.ts.map +1 -1
- package/dist/db/integrity.d.ts +20 -0
- package/dist/db/integrity.d.ts.map +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +562 -132
- package/dist/lib/context-packs.d.ts +1 -0
- package/dist/lib/context-packs.d.ts.map +1 -1
- package/dist/lib/dependency-graph.d.ts +1 -1
- package/dist/lib/doctor.d.ts +20 -0
- package/dist/lib/doctor.d.ts.map +1 -1
- package/dist/lib/integrity.d.ts +215 -0
- package/dist/lib/integrity.d.ts.map +1 -0
- package/dist/lib/native-storage-status.d.ts +2 -2
- package/dist/lib/native-storage-status.d.ts.map +1 -1
- package/dist/lib/release-compatibility.d.ts.map +1 -1
- package/dist/lib/task-dedupe.d.ts +15 -0
- package/dist/lib/task-dedupe.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +517 -85
- package/dist/mcp.js +7 -3
- package/dist/registry.d.ts +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +125 -30
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/types.d.ts +42 -0
- package/dist/sdk/types.d.ts.map +1 -1
- package/dist/sdk/v1.generated.d.ts +1 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud.d.ts +8 -1
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +521 -89
- package/dist/server/openapi.d.ts +12 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/config.d.ts +23 -5
- package/dist/storage/config.d.ts.map +1 -1
- package/dist/storage/factory.d.ts +9 -3
- package/dist/storage/factory.d.ts.map +1 -1
- package/dist/storage/hybrid.d.ts +7 -0
- package/dist/storage/hybrid.d.ts.map +1 -1
- package/dist/storage/index.d.ts +3 -3
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/interfaces.d.ts +29 -8
- 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.d.ts +3 -3
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +601 -250
- package/dist/testing.d.ts +79 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +107 -0
- package/dist/types/index.d.ts +12 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +7 -3
package/dist/mcp/index.js
CHANGED
|
@@ -44,7 +44,7 @@ var __require = import.meta.require;
|
|
|
44
44
|
function isBlockingDependencyStatus(status) {
|
|
45
45
|
return status !== "completed" && status !== "cancelled";
|
|
46
46
|
}
|
|
47
|
-
var TASK_STATUSES, VersionConflictError, TaskNotFoundError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
|
|
47
|
+
var TASK_STATUSES, VersionConflictError, TaskNotFoundError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
|
|
48
48
|
var init_types = __esm(() => {
|
|
49
49
|
TASK_STATUSES = [
|
|
50
50
|
"pending",
|
|
@@ -77,6 +77,22 @@ var init_types = __esm(() => {
|
|
|
77
77
|
this.name = "TaskNotFoundError";
|
|
78
78
|
}
|
|
79
79
|
};
|
|
80
|
+
TaskReferenceAmbiguousError = class TaskReferenceAmbiguousError extends Error {
|
|
81
|
+
reference;
|
|
82
|
+
static code = "TASK_REFERENCE_AMBIGUOUS";
|
|
83
|
+
candidateTaskIds;
|
|
84
|
+
candidateProjectIds;
|
|
85
|
+
constructor(reference, candidates) {
|
|
86
|
+
const candidateTaskIds = [...new Set(candidates.map((candidate) => candidate.task_id))].sort();
|
|
87
|
+
const candidateProjectIds = [...new Set(candidates.map((candidate) => candidate.project_id).filter((projectId) => typeof projectId === "string" && projectId.length > 0))].sort();
|
|
88
|
+
const projectList = candidateProjectIds.length > 0 ? candidateProjectIds.join(", ") : "(unscoped)";
|
|
89
|
+
super(`Task reference is ambiguous: "${reference}". Candidate project IDs: ${projectList}. ` + "Use a full task UUID.");
|
|
90
|
+
this.reference = reference;
|
|
91
|
+
this.name = "TaskReferenceAmbiguousError";
|
|
92
|
+
this.candidateTaskIds = candidateTaskIds;
|
|
93
|
+
this.candidateProjectIds = candidateProjectIds;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
80
96
|
ProjectNotFoundError = class ProjectNotFoundError extends Error {
|
|
81
97
|
projectId;
|
|
82
98
|
static code = "PROJECT_NOT_FOUND";
|
|
@@ -3766,15 +3782,18 @@ var init_identity_mapping = __esm(() => {
|
|
|
3766
3782
|
var exports_config = {};
|
|
3767
3783
|
__export(exports_config, {
|
|
3768
3784
|
parseStorageMode: () => parseStorageMode,
|
|
3785
|
+
parseStorageBackend: () => parseStorageBackend,
|
|
3769
3786
|
loadTodosStorageConfig: () => loadTodosStorageConfig,
|
|
3770
3787
|
loadStorageConfig: () => loadStorageConfig,
|
|
3771
3788
|
isTodosShadowEnabled: () => isTodosShadowEnabled,
|
|
3772
3789
|
isTodosRemoteStorageEnabled: () => isTodosRemoteStorageEnabled,
|
|
3790
|
+
isTodosPostgresBackend: () => isTodosPostgresBackend,
|
|
3773
3791
|
getTodosStorageShadowEnvName: () => getTodosStorageShadowEnvName,
|
|
3774
3792
|
getTodosStorageMode: () => getTodosStorageMode,
|
|
3775
3793
|
getTodosStorageEnvName: () => getTodosStorageEnvName,
|
|
3776
3794
|
getTodosStorageDatabaseUrl: () => getTodosStorageDatabaseUrl,
|
|
3777
3795
|
getTodosStorageDatabaseEnv: () => getTodosStorageDatabaseEnv,
|
|
3796
|
+
getTodosStorageBackend: () => getTodosStorageBackend,
|
|
3778
3797
|
getStorageMode: () => getStorageMode,
|
|
3779
3798
|
getStorageDatabaseUrl: () => getStorageDatabaseUrl,
|
|
3780
3799
|
getStorageDatabaseEnv: () => getStorageDatabaseEnv,
|
|
@@ -3799,7 +3818,7 @@ function getCanonicalTodosRdsConfig(env = process.env) {
|
|
|
3799
3818
|
};
|
|
3800
3819
|
}
|
|
3801
3820
|
function loadTodosStorageConfig(env = process.env) {
|
|
3802
|
-
const mode =
|
|
3821
|
+
const mode = getTodosStorageBackend(env);
|
|
3803
3822
|
const databaseUrl = getTodosStorageDatabaseUrl(env);
|
|
3804
3823
|
const bucket = readStorageEnv(env, "s3Bucket").value;
|
|
3805
3824
|
const prefix = readStorageEnv(env, "s3Prefix").value ?? "todos/";
|
|
@@ -3836,29 +3855,41 @@ function loadTodosStorageConfig(env = process.env) {
|
|
|
3836
3855
|
function loadStorageConfig(env = process.env) {
|
|
3837
3856
|
return loadTodosStorageConfig(env);
|
|
3838
3857
|
}
|
|
3858
|
+
function isTodosPostgresBackend(config) {
|
|
3859
|
+
return config.mode === "postgres";
|
|
3860
|
+
}
|
|
3839
3861
|
function isTodosRemoteStorageEnabled(config) {
|
|
3840
|
-
return config
|
|
3862
|
+
return isTodosPostgresBackend(config);
|
|
3841
3863
|
}
|
|
3842
3864
|
function assertTodosRemoteStorageConfig(config) {
|
|
3843
|
-
if (!
|
|
3865
|
+
if (!isTodosPostgresBackend(config))
|
|
3844
3866
|
return;
|
|
3845
3867
|
if (!config.database?.url) {
|
|
3846
3868
|
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${TODOS_STORAGE_ENV.mode}=${config.mode}`);
|
|
3847
3869
|
}
|
|
3848
3870
|
}
|
|
3849
|
-
function
|
|
3871
|
+
function parseStorageBackend(value) {
|
|
3850
3872
|
const normalized = clean(value)?.toLowerCase();
|
|
3851
3873
|
if (!normalized)
|
|
3852
|
-
return "
|
|
3853
|
-
if (normalized === "
|
|
3874
|
+
return "sqlite";
|
|
3875
|
+
if (normalized === "sqlite" || normalized === "postgres")
|
|
3854
3876
|
return normalized;
|
|
3855
|
-
|
|
3877
|
+
const legacy = LEGACY_BACKEND_TOKENS[normalized];
|
|
3878
|
+
if (legacy)
|
|
3879
|
+
return legacy;
|
|
3880
|
+
throw new Error(`${TODOS_STORAGE_ENV.mode} must be sqlite or postgres (legacy values local and remote are accepted)`);
|
|
3881
|
+
}
|
|
3882
|
+
function parseStorageMode(value) {
|
|
3883
|
+
return parseStorageBackend(value);
|
|
3884
|
+
}
|
|
3885
|
+
function getTodosStorageBackend(env = process.env) {
|
|
3886
|
+
return parseStorageBackend(readStorageEnv(env, "mode").value);
|
|
3856
3887
|
}
|
|
3857
3888
|
function getTodosStorageMode(env = process.env) {
|
|
3858
|
-
return
|
|
3889
|
+
return getTodosStorageBackend(env);
|
|
3859
3890
|
}
|
|
3860
3891
|
function getStorageMode(env = process.env) {
|
|
3861
|
-
return
|
|
3892
|
+
return getTodosStorageBackend(env);
|
|
3862
3893
|
}
|
|
3863
3894
|
function isTodosShadowEnabled(env = process.env) {
|
|
3864
3895
|
return parseBoolean(readStorageEnv(env, "shadow").value, false);
|
|
@@ -3869,8 +3900,8 @@ function getTodosStorageShadowEnvName(env = process.env) {
|
|
|
3869
3900
|
function assertTodosShadowConfig(config, env = process.env) {
|
|
3870
3901
|
if (!isTodosShadowEnabled(env))
|
|
3871
3902
|
return;
|
|
3872
|
-
if (config.mode !== "
|
|
3873
|
-
throw new Error(`${readStorageEnv(env, "shadow").name} shadow mirror requires
|
|
3903
|
+
if (config.mode !== "sqlite") {
|
|
3904
|
+
throw new Error(`${readStorageEnv(env, "shadow").name} shadow mirror requires the sqlite backend (got ${config.mode})`);
|
|
3874
3905
|
}
|
|
3875
3906
|
if (!config.database?.url) {
|
|
3876
3907
|
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${readStorageEnv(env, "shadow").name} is enabled`);
|
|
@@ -3926,7 +3957,7 @@ function parsePositiveInteger(value, fallback) {
|
|
|
3926
3957
|
}
|
|
3927
3958
|
return parsed;
|
|
3928
3959
|
}
|
|
3929
|
-
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";
|
|
3960
|
+
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", LEGACY_BACKEND_TOKENS;
|
|
3930
3961
|
var init_config = __esm(() => {
|
|
3931
3962
|
TODOS_STORAGE_TABLES = [
|
|
3932
3963
|
"todos_sync_records",
|
|
@@ -3967,6 +3998,14 @@ var init_config = __esm(() => {
|
|
|
3967
3998
|
syncBatchSize: "TODOS_SYNC_BATCH_SIZE",
|
|
3968
3999
|
syncDryRun: "TODOS_SYNC_DRY_RUN"
|
|
3969
4000
|
};
|
|
4001
|
+
LEGACY_BACKEND_TOKENS = {
|
|
4002
|
+
local: "sqlite",
|
|
4003
|
+
remote: "postgres",
|
|
4004
|
+
postgresql: "postgres",
|
|
4005
|
+
hybrid: "postgres",
|
|
4006
|
+
self_hosted: "postgres",
|
|
4007
|
+
cloud: "postgres"
|
|
4008
|
+
};
|
|
3970
4009
|
});
|
|
3971
4010
|
|
|
3972
4011
|
// src/storage/shadow-outbox-schema.ts
|
|
@@ -4100,6 +4139,30 @@ function findGitRoot(startDir) {
|
|
|
4100
4139
|
}
|
|
4101
4140
|
return null;
|
|
4102
4141
|
}
|
|
4142
|
+
function getGlobalDbPath() {
|
|
4143
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
4144
|
+
return join(home, ".hasna", "todos", "todos.db");
|
|
4145
|
+
}
|
|
4146
|
+
function hasExplicitProjectArg(args = process.argv.slice(2)) {
|
|
4147
|
+
return args.some((arg) => arg === "--project" || arg.startsWith("--project="));
|
|
4148
|
+
}
|
|
4149
|
+
function getCliCommand(args = process.argv.slice(2)) {
|
|
4150
|
+
const globalOptionsWithValues = new Set(["--project", "--agent", "--session"]);
|
|
4151
|
+
for (let index = 0;index < args.length; index += 1) {
|
|
4152
|
+
const arg = args[index];
|
|
4153
|
+
if (globalOptionsWithValues.has(arg)) {
|
|
4154
|
+
index += 1;
|
|
4155
|
+
continue;
|
|
4156
|
+
}
|
|
4157
|
+
if (arg.startsWith("-"))
|
|
4158
|
+
continue;
|
|
4159
|
+
return arg;
|
|
4160
|
+
}
|
|
4161
|
+
return null;
|
|
4162
|
+
}
|
|
4163
|
+
function canCreateScopedProjectDb(args = process.argv.slice(2)) {
|
|
4164
|
+
return getCliCommand(args) === "project-bootstrap" && !args.some((arg) => arg === "--dry-run" || arg.startsWith("--dry-run="));
|
|
4165
|
+
}
|
|
4103
4166
|
function getDbPath() {
|
|
4104
4167
|
if (process.env["HASNA_TODOS_DB_PATH"]) {
|
|
4105
4168
|
return process.env["HASNA_TODOS_DB_PATH"];
|
|
@@ -4107,18 +4170,19 @@ function getDbPath() {
|
|
|
4107
4170
|
if (process.env["TODOS_DB_PATH"]) {
|
|
4108
4171
|
return process.env["TODOS_DB_PATH"];
|
|
4109
4172
|
}
|
|
4173
|
+
if (hasExplicitProjectArg())
|
|
4174
|
+
return getGlobalDbPath();
|
|
4110
4175
|
const cwd = process.cwd();
|
|
4111
4176
|
const nearest = findNearestProjectDb(cwd);
|
|
4112
4177
|
if (nearest)
|
|
4113
4178
|
return nearest;
|
|
4114
4179
|
if (process.env["TODOS_DB_SCOPE"] === "project") {
|
|
4115
4180
|
const gitRoot = findGitRoot(cwd);
|
|
4116
|
-
if (gitRoot) {
|
|
4181
|
+
if (gitRoot && canCreateScopedProjectDb()) {
|
|
4117
4182
|
return join(gitRoot, ".hasna", "todos", "todos.db");
|
|
4118
4183
|
}
|
|
4119
4184
|
}
|
|
4120
|
-
|
|
4121
|
-
return join(home, ".hasna", "todos", "todos.db");
|
|
4185
|
+
return getGlobalDbPath();
|
|
4122
4186
|
}
|
|
4123
4187
|
function getDatabasePath() {
|
|
4124
4188
|
return getDbPath();
|
|
@@ -4221,10 +4285,13 @@ function resolvePartialId(db, table, partialId) {
|
|
|
4221
4285
|
return null;
|
|
4222
4286
|
}
|
|
4223
4287
|
if (table === "tasks") {
|
|
4224
|
-
const shortIdRows = db.query("SELECT id FROM tasks WHERE short_id = ?").all(partialId);
|
|
4288
|
+
const shortIdRows = db.query("SELECT id, project_id FROM tasks WHERE LOWER(short_id) = LOWER(?) ORDER BY project_id, id").all(partialId);
|
|
4225
4289
|
if (shortIdRows.length === 1) {
|
|
4226
4290
|
return shortIdRows[0].id;
|
|
4227
4291
|
}
|
|
4292
|
+
if (shortIdRows.length > 1) {
|
|
4293
|
+
throw new TaskReferenceAmbiguousError(partialId, shortIdRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
|
|
4294
|
+
}
|
|
4228
4295
|
}
|
|
4229
4296
|
if (table === "task_lists") {
|
|
4230
4297
|
const slugRow = db.query("SELECT id FROM task_lists WHERE slug = ?").get(partialId);
|
|
@@ -20022,9 +20089,6 @@ function cleanMode(value) {
|
|
|
20022
20089
|
const normalized = value?.trim().toLowerCase();
|
|
20023
20090
|
return normalized || null;
|
|
20024
20091
|
}
|
|
20025
|
-
function modeRole(mode) {
|
|
20026
|
-
return mode === "local" ? "local" : "remote";
|
|
20027
|
-
}
|
|
20028
20092
|
function resolveTodosCliStorageMode(env = process.env) {
|
|
20029
20093
|
for (const source of ["HASNA_TODOS_STORAGE_MODE", "TODOS_STORAGE_MODE"]) {
|
|
20030
20094
|
if (env[source] !== undefined && env[source].trim() === "") {
|
|
@@ -20037,22 +20101,25 @@ function resolveTodosCliStorageMode(env = process.env) {
|
|
|
20037
20101
|
["HASNA_TODOS_STORAGE_MODE", canonical],
|
|
20038
20102
|
["TODOS_STORAGE_MODE", fallback]
|
|
20039
20103
|
]) {
|
|
20040
|
-
if (value && !
|
|
20041
|
-
throw new Error(`REMOTE_STORAGE_MODE_INVALID: ${source}=${value} must be local
|
|
20104
|
+
if (value && !(value in TRANSPORT_TOKENS)) {
|
|
20105
|
+
throw new Error(`REMOTE_STORAGE_MODE_INVALID: ${source}=${value} must be sqlite (local file) or http (hosted /v1 authority); ` + "legacy values local and remote are accepted; " + "local SQLite fallback is disabled for invalid routing state");
|
|
20042
20106
|
}
|
|
20043
20107
|
}
|
|
20044
|
-
|
|
20108
|
+
const canonicalTransport = canonical ? TRANSPORT_TOKENS[canonical] : null;
|
|
20109
|
+
const fallbackTransport = fallback ? TRANSPORT_TOKENS[fallback] : null;
|
|
20110
|
+
if (canonicalTransport && fallbackTransport && canonicalTransport !== fallbackTransport) {
|
|
20045
20111
|
throw new Error(`REMOTE_STORAGE_MODE_CONFLICT: HASNA_TODOS_STORAGE_MODE=${canonical} conflicts with ` + `TODOS_STORAGE_MODE=${fallback}; local SQLite fallback is disabled`);
|
|
20046
20112
|
}
|
|
20047
|
-
const
|
|
20113
|
+
const transport = canonicalTransport ?? fallbackTransport ?? "sqlite";
|
|
20048
20114
|
return {
|
|
20049
|
-
mode,
|
|
20050
|
-
|
|
20115
|
+
mode: transport,
|
|
20116
|
+
transport,
|
|
20117
|
+
selected: transport === "http",
|
|
20051
20118
|
source: canonical ? "HASNA_TODOS_STORAGE_MODE" : fallback ? "TODOS_STORAGE_MODE" : "default"
|
|
20052
20119
|
};
|
|
20053
20120
|
}
|
|
20054
|
-
function
|
|
20055
|
-
return resolveTodosCliStorageMode(env).
|
|
20121
|
+
function requestedTransport(env) {
|
|
20122
|
+
return resolveTodosCliStorageMode(env).transport;
|
|
20056
20123
|
}
|
|
20057
20124
|
function normalizeRemoteAuthorityUrl(value) {
|
|
20058
20125
|
const trimmed = value?.trim();
|
|
@@ -20105,7 +20172,7 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env) {
|
|
|
20105
20172
|
return {
|
|
20106
20173
|
selected: false,
|
|
20107
20174
|
ok: true,
|
|
20108
|
-
mode
|
|
20175
|
+
mode,
|
|
20109
20176
|
api_url_configured: false,
|
|
20110
20177
|
api_key_configured: false,
|
|
20111
20178
|
v1_base_url: null,
|
|
@@ -20217,8 +20284,7 @@ async function requiredRemoteRoute(client, route, request) {
|
|
|
20217
20284
|
}
|
|
20218
20285
|
}
|
|
20219
20286
|
function getTodosCloudClient(env = process.env) {
|
|
20220
|
-
|
|
20221
|
-
if (!CLOUD_MODES.has(mode))
|
|
20287
|
+
if (requestedTransport(env) !== "http")
|
|
20222
20288
|
return null;
|
|
20223
20289
|
const resolved = resolveStorageClient("todos", requireTodosRemoteAuthorityEnv(env), {
|
|
20224
20290
|
fetchImpl: (input, init) => globalThis.fetch(input, { ...init, redirect: "manual" })
|
|
@@ -20253,13 +20319,38 @@ function toListQuery(filter = {}) {
|
|
|
20253
20319
|
query["assigned_to"] = filter.assigned_to;
|
|
20254
20320
|
if (filter.agent_id)
|
|
20255
20321
|
query["agent_id"] = filter.agent_id;
|
|
20322
|
+
if (filter.tags?.length)
|
|
20323
|
+
query["tags"] = filter.tags.join(",");
|
|
20256
20324
|
if (typeof filter.limit === "number")
|
|
20257
20325
|
query["limit"] = filter.limit;
|
|
20258
20326
|
if (typeof filter.offset === "number")
|
|
20259
20327
|
query["offset"] = filter.offset;
|
|
20260
20328
|
return query;
|
|
20261
20329
|
}
|
|
20330
|
+
async function fetchListTagsCapability(client) {
|
|
20331
|
+
const document = await requiredRemoteRoute(client, "/v1/openapi.json", () => client.transport.get("/openapi.json"));
|
|
20332
|
+
if (!document || typeof document !== "object" || Array.isArray(document))
|
|
20333
|
+
return false;
|
|
20334
|
+
const paths = document["paths"];
|
|
20335
|
+
const tasksPath = paths && typeof paths === "object" && !Array.isArray(paths) ? paths["/v1/tasks"] : undefined;
|
|
20336
|
+
const get = tasksPath && typeof tasksPath === "object" && !Array.isArray(tasksPath) ? tasksPath["get"] : undefined;
|
|
20337
|
+
const parameters = get && typeof get === "object" && !Array.isArray(get) ? get["parameters"] : undefined;
|
|
20338
|
+
return Array.isArray(parameters) && parameters.some((parameter) => parameter && typeof parameter === "object" && parameter["name"] === "tags");
|
|
20339
|
+
}
|
|
20340
|
+
async function requireTagsFilterCapability(client) {
|
|
20341
|
+
const authority = remoteAuthorityBase(client);
|
|
20342
|
+
let capability = listTagsCapabilityCache.get(authority);
|
|
20343
|
+
if (!capability) {
|
|
20344
|
+
capability = fetchListTagsCapability(client);
|
|
20345
|
+
listTagsCapabilityCache.set(authority, capability);
|
|
20346
|
+
}
|
|
20347
|
+
if (!await capability) {
|
|
20348
|
+
throw new Error(`REMOTE_TAGS_FILTER_UNSUPPORTED: configured Todos authority ${authority} does not advertise the tags ` + "query param on GET /v1/tasks; deploy the current @hasna/todos /v1 server to filter by tag; " + "no unfiltered task read was issued");
|
|
20349
|
+
}
|
|
20350
|
+
}
|
|
20262
20351
|
async function cloudListTasks(client, filter = {}) {
|
|
20352
|
+
if (filter.tags?.length)
|
|
20353
|
+
await requireTagsFilterCapability(client);
|
|
20263
20354
|
const res = await requiredRemoteRoute(client, "/v1/tasks", () => client.list("tasks", { query: toListQuery(filter) }));
|
|
20264
20355
|
const envelope = res.raw;
|
|
20265
20356
|
return Array.isArray(envelope?.tasks) ? envelope.tasks : res.items;
|
|
@@ -20367,6 +20458,8 @@ function redactComment(comment) {
|
|
|
20367
20458
|
}
|
|
20368
20459
|
async function cloudCountTasks(client, filter = {}) {
|
|
20369
20460
|
const { limit: _drop, offset: _o, ...rest } = filter;
|
|
20461
|
+
if (rest.tags?.length)
|
|
20462
|
+
await requireTagsFilterCapability(client);
|
|
20370
20463
|
const res = await requiredRemoteRoute(client, "/v1/tasks", () => client.list("tasks", { query: { ...toListQuery(rest), limit: 1 } }));
|
|
20371
20464
|
const envelope = res.raw;
|
|
20372
20465
|
if (typeof envelope?.total === "number")
|
|
@@ -20429,15 +20522,23 @@ async function cloudResolveTaskListRef(client, ref, projectId) {
|
|
|
20429
20522
|
}
|
|
20430
20523
|
throw new Error(`Task list not found: "${input}"`);
|
|
20431
20524
|
}
|
|
20432
|
-
var UUID_RE,
|
|
20525
|
+
var UUID_RE, TRANSPORT_TOKENS, completionCapabilityCache, listTagsCapabilityCache;
|
|
20433
20526
|
var init_cloud_router = __esm(() => {
|
|
20434
20527
|
init_types();
|
|
20435
20528
|
init_redaction();
|
|
20436
20529
|
init_http_client();
|
|
20437
20530
|
UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
20438
|
-
|
|
20439
|
-
|
|
20531
|
+
TRANSPORT_TOKENS = {
|
|
20532
|
+
sqlite: "sqlite",
|
|
20533
|
+
http: "http",
|
|
20534
|
+
local: "sqlite",
|
|
20535
|
+
remote: "http",
|
|
20536
|
+
self_hosted: "http",
|
|
20537
|
+
cloud: "http",
|
|
20538
|
+
hybrid: "http"
|
|
20539
|
+
};
|
|
20440
20540
|
completionCapabilityCache = new Map;
|
|
20541
|
+
listTagsCapabilityCache = new Map;
|
|
20441
20542
|
});
|
|
20442
20543
|
|
|
20443
20544
|
// src/mcp/tools/task-crud.ts
|
|
@@ -24023,12 +24124,11 @@ ${task.description || ""}`.split(/\r?\n/);
|
|
|
24023
24124
|
function normalizeStackLocation(value) {
|
|
24024
24125
|
return value.replace(/:\d+(?::\d+)?/g, "").replace(/^file:\/\//, "").toLowerCase().trim();
|
|
24025
24126
|
}
|
|
24026
|
-
function
|
|
24127
|
+
function fingerprintContent(task) {
|
|
24027
24128
|
const body = task.description || "";
|
|
24028
24129
|
const text = `${task.title}
|
|
24029
24130
|
${body}`;
|
|
24030
24131
|
return {
|
|
24031
|
-
task,
|
|
24032
24132
|
title: normalizeText(task.title),
|
|
24033
24133
|
body: normalizeText(body),
|
|
24034
24134
|
text: normalizeText(text),
|
|
@@ -24037,6 +24137,9 @@ ${body}`;
|
|
|
24037
24137
|
stackKeys: stackKeysFor(task)
|
|
24038
24138
|
};
|
|
24039
24139
|
}
|
|
24140
|
+
function fingerprint(task) {
|
|
24141
|
+
return { task, ...fingerprintContent(task) };
|
|
24142
|
+
}
|
|
24040
24143
|
function intersects(left, right) {
|
|
24041
24144
|
for (const value of left)
|
|
24042
24145
|
if (right.has(value))
|
|
@@ -25373,7 +25476,7 @@ function createReleaseCompatibilityReport(options = {}) {
|
|
|
25373
25476
|
"todos --help",
|
|
25374
25477
|
"todos-mcp --help",
|
|
25375
25478
|
"todos-serve --help",
|
|
25376
|
-
"todos doctor"
|
|
25479
|
+
"todos doctor --no-fail-on-findings"
|
|
25377
25480
|
],
|
|
25378
25481
|
smoke_tests: [
|
|
25379
25482
|
"command -v todos",
|
|
@@ -25381,7 +25484,7 @@ function createReleaseCompatibilityReport(options = {}) {
|
|
|
25381
25484
|
"command -v todos-serve",
|
|
25382
25485
|
"todos --version",
|
|
25383
25486
|
"todos --help",
|
|
25384
|
-
"todos doctor"
|
|
25487
|
+
"todos doctor --no-fail-on-findings"
|
|
25385
25488
|
],
|
|
25386
25489
|
rollback: [
|
|
25387
25490
|
"npm view @hasna/todos versions --json",
|
|
@@ -29027,6 +29130,211 @@ var init_auto_assign = __esm(() => {
|
|
|
29027
29130
|
init_agents();
|
|
29028
29131
|
});
|
|
29029
29132
|
|
|
29133
|
+
// src/lib/integrity.ts
|
|
29134
|
+
function resolveIntegritySeverity(spec, measurement) {
|
|
29135
|
+
if (spec.kind === "dangling")
|
|
29136
|
+
return "error";
|
|
29137
|
+
if (spec.escalate_when_open && (measurement.open_count ?? 0) > 0)
|
|
29138
|
+
return "error";
|
|
29139
|
+
return spec.base_severity;
|
|
29140
|
+
}
|
|
29141
|
+
function plural(entity, count) {
|
|
29142
|
+
const [one, many] = ENTITY_LABEL[entity];
|
|
29143
|
+
return count === 1 ? one : many;
|
|
29144
|
+
}
|
|
29145
|
+
function formatIntegrityMessage(spec, measurement) {
|
|
29146
|
+
const noun = plural(spec.entity, measurement.count);
|
|
29147
|
+
const verb = measurement.count === 1 ? ["has", "references"] : ["have", "reference"];
|
|
29148
|
+
const open = measurement.open_count === null || measurement.open_count === 0 ? "" : ` (${measurement.open_count} still open)`;
|
|
29149
|
+
return spec.kind === "missing" ? `${measurement.count} ${noun} ${verb[0]} no ${spec.field}${open}` : `${measurement.count} ${noun} ${verb[1]} a ${spec.target.replace("_", " ")} that is not registered${open}`;
|
|
29150
|
+
}
|
|
29151
|
+
function measuredCondition(spec, measurement, source) {
|
|
29152
|
+
return {
|
|
29153
|
+
id: spec.id,
|
|
29154
|
+
entity: spec.entity,
|
|
29155
|
+
field: spec.field,
|
|
29156
|
+
kind: spec.kind,
|
|
29157
|
+
count: measurement.count,
|
|
29158
|
+
open_count: measurement.open_count,
|
|
29159
|
+
severity: measurement.count > 0 ? resolveIntegritySeverity(spec, measurement) : null,
|
|
29160
|
+
verified: true,
|
|
29161
|
+
source,
|
|
29162
|
+
message: formatIntegrityMessage(spec, measurement),
|
|
29163
|
+
impact: spec.impact
|
|
29164
|
+
};
|
|
29165
|
+
}
|
|
29166
|
+
function unverifiedCondition(spec, reason) {
|
|
29167
|
+
return {
|
|
29168
|
+
id: spec.id,
|
|
29169
|
+
entity: spec.entity,
|
|
29170
|
+
field: spec.field,
|
|
29171
|
+
kind: spec.kind,
|
|
29172
|
+
count: null,
|
|
29173
|
+
open_count: null,
|
|
29174
|
+
severity: null,
|
|
29175
|
+
verified: false,
|
|
29176
|
+
source: "unverified",
|
|
29177
|
+
unverified_reason: reason,
|
|
29178
|
+
message: `${spec.id}: NOT CHECKED \u2014 ${reason}`,
|
|
29179
|
+
impact: spec.impact
|
|
29180
|
+
};
|
|
29181
|
+
}
|
|
29182
|
+
function summarizeIntegrity(conditions) {
|
|
29183
|
+
const measured = conditions.filter((condition) => condition.verified && condition.count !== null);
|
|
29184
|
+
const findings = measured.filter((condition) => (condition.count ?? 0) > 0);
|
|
29185
|
+
const unverified = conditions.length - measured.length;
|
|
29186
|
+
return {
|
|
29187
|
+
ok: unverified === 0 && findings.length === 0,
|
|
29188
|
+
findings: findings.length,
|
|
29189
|
+
rows: findings.reduce((total, condition) => total + (condition.count ?? 0), 0),
|
|
29190
|
+
errors: findings.filter((condition) => condition.severity === "error").length,
|
|
29191
|
+
warnings: findings.filter((condition) => condition.severity === "warn").length,
|
|
29192
|
+
unverified,
|
|
29193
|
+
complete: unverified === 0
|
|
29194
|
+
};
|
|
29195
|
+
}
|
|
29196
|
+
function buildIntegrityReport(conditions, generatedAt) {
|
|
29197
|
+
const measuredSources = [...new Set(conditions.map((condition) => condition.source))].filter((source) => source !== "unverified");
|
|
29198
|
+
return {
|
|
29199
|
+
schema_version: TODOS_INTEGRITY_SCHEMA_VERSION,
|
|
29200
|
+
generated_at: generatedAt,
|
|
29201
|
+
source: measuredSources.length === 0 ? "unverified" : measuredSources.length === 1 ? measuredSources[0] : "remote-derived",
|
|
29202
|
+
conditions,
|
|
29203
|
+
summary: summarizeIntegrity(conditions)
|
|
29204
|
+
};
|
|
29205
|
+
}
|
|
29206
|
+
function sqliteOpenStatusList() {
|
|
29207
|
+
return OPEN_TASK_STATUSES.map((status) => `'${status}'`).join(", ");
|
|
29208
|
+
}
|
|
29209
|
+
function buildSqliteIntegritySql(spec) {
|
|
29210
|
+
const table = SQLITE_TABLE[spec.entity];
|
|
29211
|
+
const target = SQLITE_TABLE[spec.target];
|
|
29212
|
+
const column = `t."${spec.field}"`;
|
|
29213
|
+
const predicate = spec.kind === "missing" ? `(${column} IS NULL OR ${column} = '')` : `${column} IS NOT NULL AND ${column} <> '' ` + `AND NOT EXISTS (SELECT 1 FROM "${target}" r WHERE r."id" = ${column})`;
|
|
29214
|
+
const openExpr = spec.entity === "task" ? `SUM(CASE WHEN t."status" IN (${sqliteOpenStatusList()}) THEN 1 ELSE 0 END)` : "NULL";
|
|
29215
|
+
return `SELECT COUNT(*) AS count, ${openExpr} AS open_count FROM "${table}" t WHERE ${predicate}`;
|
|
29216
|
+
}
|
|
29217
|
+
function buildPostgresIntegritySql(spec, options) {
|
|
29218
|
+
const params = [options.service, RECORD_TYPE[spec.entity]];
|
|
29219
|
+
const p = (value) => {
|
|
29220
|
+
params.push(value);
|
|
29221
|
+
return `$${params.length}`;
|
|
29222
|
+
};
|
|
29223
|
+
const column = `t.payload->>'${spec.field}'`;
|
|
29224
|
+
const predicate = spec.kind === "missing" ? `(${column} IS NULL OR ${column} = '')` : `${column} IS NOT NULL AND ${column} <> '' AND NOT EXISTS (` + `SELECT 1 FROM ${options.table} r WHERE r.service = $1 AND r.object_type = ${p(RECORD_TYPE[spec.target])} ` + `AND r.deleted_at IS NULL AND r.object_id = ${column})`;
|
|
29225
|
+
const openExpr = spec.entity === "task" ? `COUNT(*) FILTER (WHERE t.payload->>'status' IN (${OPEN_TASK_STATUSES.map((status) => p(status)).join(", ")}))::int` : "NULL::int";
|
|
29226
|
+
const sql = `/* todos:integrity-${spec.id} */ SELECT COUNT(*)::int AS count, ${openExpr} AS open_count ` + `FROM ${options.table} t WHERE t.service = $1 AND t.object_type = $2 AND t.deleted_at IS NULL AND ${predicate}`;
|
|
29227
|
+
return { sql, params };
|
|
29228
|
+
}
|
|
29229
|
+
var TODOS_INTEGRITY_SCHEMA_VERSION = "todos.integrity.v1", TERMINAL_TASK_STATUSES, OPEN_TASK_STATUSES, INTEGRITY_CONDITIONS, ENTITY_LABEL, SQLITE_TABLE, RECORD_TYPE;
|
|
29230
|
+
var init_integrity = __esm(() => {
|
|
29231
|
+
init_types();
|
|
29232
|
+
TERMINAL_TASK_STATUSES = ["completed", "failed", "cancelled"];
|
|
29233
|
+
OPEN_TASK_STATUSES = TASK_STATUSES.filter((status) => !TERMINAL_TASK_STATUSES.includes(status));
|
|
29234
|
+
INTEGRITY_CONDITIONS = [
|
|
29235
|
+
{
|
|
29236
|
+
id: "tasks_without_project",
|
|
29237
|
+
entity: "task",
|
|
29238
|
+
field: "project_id",
|
|
29239
|
+
target: "project",
|
|
29240
|
+
kind: "missing",
|
|
29241
|
+
base_severity: "warn",
|
|
29242
|
+
escalate_when_open: true,
|
|
29243
|
+
impact: "invisible to every project-scoped read (list, status, next, claim)"
|
|
29244
|
+
},
|
|
29245
|
+
{
|
|
29246
|
+
id: "tasks_without_task_list",
|
|
29247
|
+
entity: "task",
|
|
29248
|
+
field: "task_list_id",
|
|
29249
|
+
target: "task_list",
|
|
29250
|
+
kind: "missing",
|
|
29251
|
+
base_severity: "warn",
|
|
29252
|
+
escalate_when_open: true,
|
|
29253
|
+
impact: "invisible to every task-list read \u2014 the list reports zero open work"
|
|
29254
|
+
},
|
|
29255
|
+
{
|
|
29256
|
+
id: "tasks_with_unregistered_project",
|
|
29257
|
+
entity: "task",
|
|
29258
|
+
field: "project_id",
|
|
29259
|
+
target: "project",
|
|
29260
|
+
kind: "dangling",
|
|
29261
|
+
base_severity: "error",
|
|
29262
|
+
escalate_when_open: false,
|
|
29263
|
+
impact: "points at a project id that does not exist; the reference can never resolve"
|
|
29264
|
+
},
|
|
29265
|
+
{
|
|
29266
|
+
id: "tasks_with_unregistered_task_list",
|
|
29267
|
+
entity: "task",
|
|
29268
|
+
field: "task_list_id",
|
|
29269
|
+
target: "task_list",
|
|
29270
|
+
kind: "dangling",
|
|
29271
|
+
base_severity: "error",
|
|
29272
|
+
escalate_when_open: false,
|
|
29273
|
+
impact: "points at a task-list id that does not exist; the reference can never resolve"
|
|
29274
|
+
},
|
|
29275
|
+
{
|
|
29276
|
+
id: "task_lists_without_project",
|
|
29277
|
+
entity: "task_list",
|
|
29278
|
+
field: "project_id",
|
|
29279
|
+
target: "project",
|
|
29280
|
+
kind: "missing",
|
|
29281
|
+
base_severity: "warn",
|
|
29282
|
+
escalate_when_open: false,
|
|
29283
|
+
impact: "unbound list \u2014 unreachable from any project, so its tasks are unroutable"
|
|
29284
|
+
},
|
|
29285
|
+
{
|
|
29286
|
+
id: "task_lists_with_unregistered_project",
|
|
29287
|
+
entity: "task_list",
|
|
29288
|
+
field: "project_id",
|
|
29289
|
+
target: "project",
|
|
29290
|
+
kind: "dangling",
|
|
29291
|
+
base_severity: "error",
|
|
29292
|
+
escalate_when_open: false,
|
|
29293
|
+
impact: "points at a project id that does not exist; the list can never be reached"
|
|
29294
|
+
}
|
|
29295
|
+
];
|
|
29296
|
+
ENTITY_LABEL = {
|
|
29297
|
+
task: ["task", "tasks"],
|
|
29298
|
+
task_list: ["task list", "task lists"]
|
|
29299
|
+
};
|
|
29300
|
+
SQLITE_TABLE = {
|
|
29301
|
+
task: "tasks",
|
|
29302
|
+
task_list: "task_lists",
|
|
29303
|
+
project: "projects"
|
|
29304
|
+
};
|
|
29305
|
+
RECORD_TYPE = {
|
|
29306
|
+
task: "tasks",
|
|
29307
|
+
task_list: "task_lists",
|
|
29308
|
+
project: "projects"
|
|
29309
|
+
};
|
|
29310
|
+
});
|
|
29311
|
+
|
|
29312
|
+
// src/db/integrity.ts
|
|
29313
|
+
function tableExists(db, table) {
|
|
29314
|
+
return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
|
|
29315
|
+
}
|
|
29316
|
+
function scanSqliteIntegrity(db = getDatabase()) {
|
|
29317
|
+
const missing = Object.keys(REQUIRED_TABLES2).filter((table) => !tableExists(db, table));
|
|
29318
|
+
const conditions = INTEGRITY_CONDITIONS.map((spec) => {
|
|
29319
|
+
if (missing.length > 0) {
|
|
29320
|
+
return unverifiedCondition(spec, `local schema is missing table(s): ${missing.join(", ")}`);
|
|
29321
|
+
}
|
|
29322
|
+
try {
|
|
29323
|
+
const row = db.query(buildSqliteIntegritySql(spec)).get();
|
|
29324
|
+
return measuredCondition(spec, { count: Number(row?.count ?? 0), open_count: spec.entity === "task" ? Number(row?.open_count ?? 0) : null }, "sqlite");
|
|
29325
|
+
} catch (error) {
|
|
29326
|
+
return unverifiedCondition(spec, error instanceof Error ? error.message : String(error));
|
|
29327
|
+
}
|
|
29328
|
+
});
|
|
29329
|
+
return buildIntegrityReport(conditions, now());
|
|
29330
|
+
}
|
|
29331
|
+
var REQUIRED_TABLES2;
|
|
29332
|
+
var init_integrity2 = __esm(() => {
|
|
29333
|
+
init_database();
|
|
29334
|
+
init_integrity();
|
|
29335
|
+
REQUIRED_TABLES2 = { tasks: true, task_lists: true, projects: true };
|
|
29336
|
+
});
|
|
29337
|
+
|
|
29030
29338
|
// src/lib/doctor.ts
|
|
29031
29339
|
var exports_doctor = {};
|
|
29032
29340
|
__export(exports_doctor, {
|
|
@@ -29034,7 +29342,7 @@ __export(exports_doctor, {
|
|
|
29034
29342
|
});
|
|
29035
29343
|
import { chmodSync, copyFileSync, existsSync as existsSync10, mkdirSync as mkdirSync5, statSync as statSync5 } from "fs";
|
|
29036
29344
|
import { basename as basename4, dirname as dirname6, join as join8 } from "path";
|
|
29037
|
-
function
|
|
29345
|
+
function tableExists2(db, table) {
|
|
29038
29346
|
return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
|
|
29039
29347
|
}
|
|
29040
29348
|
function quoteIdent(identifier) {
|
|
@@ -29118,7 +29426,7 @@ function findDuplicateIndexes(db) {
|
|
|
29118
29426
|
return duplicates;
|
|
29119
29427
|
}
|
|
29120
29428
|
function findMissingProjectRoots(db) {
|
|
29121
|
-
if (!
|
|
29429
|
+
if (!tableExists2(db, "projects"))
|
|
29122
29430
|
return 0;
|
|
29123
29431
|
let missing = 0;
|
|
29124
29432
|
const rows = db.query("SELECT path FROM projects WHERE path IS NOT NULL AND path != ''").all();
|
|
@@ -29133,7 +29441,7 @@ function findMissingProjectRoots(db) {
|
|
|
29133
29441
|
return missing;
|
|
29134
29442
|
}
|
|
29135
29443
|
function addTaskStateChecks(db, checks) {
|
|
29136
|
-
if (!
|
|
29444
|
+
if (!tableExists2(db, "tasks"))
|
|
29137
29445
|
return;
|
|
29138
29446
|
const columns = getTableColumns(db, "tasks");
|
|
29139
29447
|
const staleCutoff = new Date(Date.now() - 30 * 60 * 1000).toISOString();
|
|
@@ -29205,15 +29513,42 @@ function createBackup(dbPath) {
|
|
|
29205
29513
|
function pushRepair(repairs, type, message, applied, count) {
|
|
29206
29514
|
repairs.push({ type, message, applied, count });
|
|
29207
29515
|
}
|
|
29208
|
-
function summarize2(checks, repairs) {
|
|
29516
|
+
function summarize2(checks, repairs, integrity) {
|
|
29209
29517
|
return {
|
|
29210
29518
|
errors: checks.filter((check) => check.severity === "error").length,
|
|
29211
29519
|
warnings: checks.filter((check) => check.severity === "warn").length,
|
|
29212
29520
|
infos: checks.filter((check) => check.severity === "info").length,
|
|
29213
29521
|
repairable: checks.filter((check) => check.repairable).length,
|
|
29214
|
-
applied: repairs.filter((repair) => repair.applied).length
|
|
29522
|
+
applied: repairs.filter((repair) => repair.applied).length,
|
|
29523
|
+
integrity_findings: integrity.summary.findings,
|
|
29524
|
+
integrity_rows: integrity.summary.rows,
|
|
29525
|
+
integrity_unverified: integrity.summary.unverified
|
|
29215
29526
|
};
|
|
29216
29527
|
}
|
|
29528
|
+
function addIntegrityChecks(integrity, checks) {
|
|
29529
|
+
for (const condition of integrity.conditions) {
|
|
29530
|
+
if (!condition.verified) {
|
|
29531
|
+
addCheck(checks, {
|
|
29532
|
+
severity: "warn",
|
|
29533
|
+
type: `${condition.id}_unverified`,
|
|
29534
|
+
message: condition.message,
|
|
29535
|
+
repairable: false,
|
|
29536
|
+
integrity: true
|
|
29537
|
+
});
|
|
29538
|
+
continue;
|
|
29539
|
+
}
|
|
29540
|
+
if ((condition.count ?? 0) === 0)
|
|
29541
|
+
continue;
|
|
29542
|
+
addCheck(checks, {
|
|
29543
|
+
severity: condition.severity ?? "warn",
|
|
29544
|
+
type: condition.id,
|
|
29545
|
+
message: `${condition.message} \u2014 ${condition.impact}`,
|
|
29546
|
+
count: condition.count ?? undefined,
|
|
29547
|
+
repairable: false,
|
|
29548
|
+
integrity: true
|
|
29549
|
+
});
|
|
29550
|
+
}
|
|
29551
|
+
}
|
|
29217
29552
|
function deleteOrphans(db, table, where) {
|
|
29218
29553
|
const before = countQuery(db, `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`);
|
|
29219
29554
|
if (before > 0)
|
|
@@ -29242,7 +29577,7 @@ function runTodosDoctor(options = {}) {
|
|
|
29242
29577
|
message: `Schema at migration ${migrationCurrent}`
|
|
29243
29578
|
});
|
|
29244
29579
|
}
|
|
29245
|
-
const missingTables =
|
|
29580
|
+
const missingTables = REQUIRED_TABLES3.filter((table) => !tableExists2(db, table));
|
|
29246
29581
|
if (missingTables.length > 0) {
|
|
29247
29582
|
addCheck(checks, {
|
|
29248
29583
|
severity: "error",
|
|
@@ -29252,7 +29587,7 @@ function runTodosDoctor(options = {}) {
|
|
|
29252
29587
|
repairable: true
|
|
29253
29588
|
});
|
|
29254
29589
|
}
|
|
29255
|
-
const orphanedParents =
|
|
29590
|
+
const orphanedParents = tableExists2(db, "tasks") ? countQuery(db, "SELECT COUNT(*) as count FROM tasks t WHERE t.parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM tasks p WHERE p.id = t.parent_id)") : 0;
|
|
29256
29591
|
if (orphanedParents > 0) {
|
|
29257
29592
|
addCheck(checks, {
|
|
29258
29593
|
severity: "error",
|
|
@@ -29262,7 +29597,7 @@ function runTodosDoctor(options = {}) {
|
|
|
29262
29597
|
repairable: true
|
|
29263
29598
|
});
|
|
29264
29599
|
}
|
|
29265
|
-
const orphanedDependencies =
|
|
29600
|
+
const orphanedDependencies = tableExists2(db, "task_dependencies") && tableExists2(db, "tasks") ? countQuery(db, "SELECT COUNT(*) as count FROM task_dependencies d WHERE NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = d.task_id) OR NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = d.depends_on)") : 0;
|
|
29266
29601
|
if (orphanedDependencies > 0) {
|
|
29267
29602
|
addCheck(checks, {
|
|
29268
29603
|
severity: "error",
|
|
@@ -29281,7 +29616,7 @@ function runTodosDoctor(options = {}) {
|
|
|
29281
29616
|
];
|
|
29282
29617
|
let orphanedRows = 0;
|
|
29283
29618
|
for (const [table, where] of orphanTables) {
|
|
29284
|
-
if (!
|
|
29619
|
+
if (!tableExists2(db, table))
|
|
29285
29620
|
continue;
|
|
29286
29621
|
orphanedRows += countQuery(db, `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`);
|
|
29287
29622
|
}
|
|
@@ -29295,6 +29630,8 @@ function runTodosDoctor(options = {}) {
|
|
|
29295
29630
|
});
|
|
29296
29631
|
}
|
|
29297
29632
|
addTaskStateChecks(db, checks);
|
|
29633
|
+
const integrity = scanSqliteIntegrity(db);
|
|
29634
|
+
addIntegrityChecks(integrity, checks);
|
|
29298
29635
|
const corruptJson = findCorruptJsonMetadata(db);
|
|
29299
29636
|
if (corruptJson.length > 0) {
|
|
29300
29637
|
addCheck(checks, {
|
|
@@ -29353,12 +29690,12 @@ function runTodosDoctor(options = {}) {
|
|
|
29353
29690
|
db.run("UPDATE tasks SET parent_id = NULL WHERE parent_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM tasks p WHERE p.id = tasks.parent_id)");
|
|
29354
29691
|
pushRepair(repairs, "orphaned_task_parents", "Cleared missing parent references", true, orphanedParents);
|
|
29355
29692
|
}
|
|
29356
|
-
if (orphanedDependencies > 0 &&
|
|
29693
|
+
if (orphanedDependencies > 0 && tableExists2(db, "task_dependencies")) {
|
|
29357
29694
|
const count = deleteOrphans(db, "task_dependencies", "NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = task_dependencies.task_id) OR NOT EXISTS (SELECT 1 FROM tasks t WHERE t.id = task_dependencies.depends_on)");
|
|
29358
29695
|
pushRepair(repairs, "orphaned_task_dependencies", "Deleted dependency rows referencing missing tasks", true, count);
|
|
29359
29696
|
}
|
|
29360
29697
|
for (const [table, where] of orphanTables) {
|
|
29361
|
-
if (!
|
|
29698
|
+
if (!tableExists2(db, table))
|
|
29362
29699
|
continue;
|
|
29363
29700
|
const count = deleteOrphans(db, table, where);
|
|
29364
29701
|
if (count > 0)
|
|
@@ -29387,26 +29724,30 @@ function runTodosDoctor(options = {}) {
|
|
|
29387
29724
|
}
|
|
29388
29725
|
}
|
|
29389
29726
|
}
|
|
29390
|
-
const
|
|
29391
|
-
const
|
|
29727
|
+
const reread = apply && hasRepairableIssue ? runTodosDoctor({ db, dbPath, apply: false }) : undefined;
|
|
29728
|
+
const finalChecks = reread?.checks ?? checks;
|
|
29729
|
+
const finalIntegrity = reread?.integrity ?? integrity;
|
|
29730
|
+
const summary = summarize2(finalChecks, repairs, finalIntegrity);
|
|
29392
29731
|
return {
|
|
29393
|
-
ok: !finalChecks.some((check) => check.severity === "error"),
|
|
29732
|
+
ok: !finalChecks.some((check) => check.severity === "error") && finalIntegrity.summary.ok,
|
|
29394
29733
|
dry_run: !apply,
|
|
29395
29734
|
database_path: dbPath,
|
|
29396
29735
|
migration: { current: getMigrationLevel(db), expected: migrationExpected },
|
|
29397
29736
|
backup,
|
|
29398
29737
|
checks: finalChecks,
|
|
29399
29738
|
repairs,
|
|
29739
|
+
integrity: finalIntegrity,
|
|
29400
29740
|
summary
|
|
29401
29741
|
};
|
|
29402
29742
|
}
|
|
29403
|
-
var
|
|
29743
|
+
var REQUIRED_TABLES3;
|
|
29404
29744
|
var init_doctor = __esm(() => {
|
|
29405
29745
|
init_database();
|
|
29746
|
+
init_integrity2();
|
|
29406
29747
|
init_migrations();
|
|
29407
29748
|
init_schema();
|
|
29408
29749
|
init_recurrence();
|
|
29409
|
-
|
|
29750
|
+
REQUIRED_TABLES3 = [
|
|
29410
29751
|
"_migrations",
|
|
29411
29752
|
"projects",
|
|
29412
29753
|
"tasks",
|
|
@@ -29750,7 +30091,7 @@ function limits(input) {
|
|
|
29750
30091
|
verification_limit: clamp(input.verification_limit, DEFAULT_LIMITS.verification_limit, 100),
|
|
29751
30092
|
run_limit: clamp(input.run_limit, DEFAULT_LIMITS.run_limit, 20),
|
|
29752
30093
|
dependency_limit: clamp(input.dependency_limit, DEFAULT_LIMITS.dependency_limit, 100),
|
|
29753
|
-
plan_task_limit: clamp(input.plan_task_limit, DEFAULT_LIMITS.plan_task_limit,
|
|
30094
|
+
plan_task_limit: clamp(input.plan_task_limit, DEFAULT_LIMITS.plan_task_limit, MAX_CONTEXT_TASKS),
|
|
29754
30095
|
max_text_chars: clamp(input.max_text_chars, DEFAULT_LIMITS.max_text_chars, 50000),
|
|
29755
30096
|
summary_char_limit: clamp(input.summary_char_limit, DEFAULT_LIMITS.summary_char_limit, 4000),
|
|
29756
30097
|
stale_after_hours: clamp(input.stale_after_hours, DEFAULT_LIMITS.stale_after_hours, 24 * 365)
|
|
@@ -29773,6 +30114,13 @@ function taskSummary(task) {
|
|
|
29773
30114
|
priority: task.priority
|
|
29774
30115
|
};
|
|
29775
30116
|
}
|
|
30117
|
+
function latestPlanTasks(planId, limit, db) {
|
|
30118
|
+
const rows = db.query(`SELECT * FROM tasks
|
|
30119
|
+
WHERE plan_id = ? AND archived_at IS NULL
|
|
30120
|
+
ORDER BY updated_at DESC, created_at DESC, id ASC
|
|
30121
|
+
LIMIT ?`).all(planId, limit);
|
|
30122
|
+
return rows.map(rowToTask);
|
|
30123
|
+
}
|
|
29776
30124
|
function acceptanceCriteria(task, maxText) {
|
|
29777
30125
|
const metadata = task.metadata || {};
|
|
29778
30126
|
const raw = metadata["acceptance_criteria"] ?? metadata["acceptanceCriteria"] ?? metadata["criteria"];
|
|
@@ -29870,7 +30218,7 @@ function summarizeSection(pack, section, maxChars) {
|
|
|
29870
30218
|
return "No plan was attached.";
|
|
29871
30219
|
return summarizeStrings([
|
|
29872
30220
|
`Plan ${pack.plan.name} is ${pack.plan.status}`,
|
|
29873
|
-
`${pack.plan.tasks.length} listed plan
|
|
30221
|
+
`${pack.plan.tasks.length} of ${pack.plan.total_tasks} plan tasks listed; ${pack.plan.omitted_tasks} omitted`,
|
|
29874
30222
|
...pack.plan.tasks.slice(0, 4).map((task) => `${task.status} ${task.short_id || task.id.slice(0, 8)} ${task.title}`)
|
|
29875
30223
|
], maxChars);
|
|
29876
30224
|
}
|
|
@@ -30020,7 +30368,8 @@ function createAgentContextPack(input, db) {
|
|
|
30020
30368
|
const limit = limits(input);
|
|
30021
30369
|
const project = task.project_id ? getProject(task.project_id, d) : null;
|
|
30022
30370
|
const plan = task.plan_id ? getPlan(task.plan_id, d) : null;
|
|
30023
|
-
const planTasks = task.plan_id ?
|
|
30371
|
+
const planTasks = task.plan_id ? latestPlanTasks(task.plan_id, limit.plan_task_limit, d) : [];
|
|
30372
|
+
const totalPlanTasks = task.plan_id ? countTasks({ plan_id: task.plan_id }, d) : 0;
|
|
30024
30373
|
const upstream = getTaskDependencies(task.id, d).map((dep) => taskSummary(getTask(dep.depends_on, d))).filter((item) => Boolean(item));
|
|
30025
30374
|
const downstream = getTaskDependents(task.id, d).map((dep) => taskSummary(getTask(dep.task_id, d))).filter((item) => Boolean(item));
|
|
30026
30375
|
const comments = listComments(task.id, d);
|
|
@@ -30063,6 +30412,9 @@ function createAgentContextPack(input, db) {
|
|
|
30063
30412
|
warnings.push(`${runs.length - selectedRuns.length} older runs omitted`);
|
|
30064
30413
|
if (fileMap.size > relevantFiles.length)
|
|
30065
30414
|
warnings.push(`${fileMap.size - relevantFiles.length} relevant files omitted`);
|
|
30415
|
+
if (totalPlanTasks > planTasks.length && plan) {
|
|
30416
|
+
warnings.push(`${totalPlanTasks - planTasks.length} plan tasks omitted (${totalPlanTasks} total); load more on demand with "todos plans --show ${plan.id}" and inspect task details with "todos show <task-id>" or "todos context-pack <task-id>"`);
|
|
30417
|
+
}
|
|
30066
30418
|
const contextTask = {
|
|
30067
30419
|
id: task.id,
|
|
30068
30420
|
short_id: task.short_id,
|
|
@@ -30094,8 +30446,9 @@ function createAgentContextPack(input, db) {
|
|
|
30094
30446
|
description: truncate2(plan.description, limit.max_text_chars),
|
|
30095
30447
|
status: plan.status,
|
|
30096
30448
|
agent_id: plan.agent_id,
|
|
30097
|
-
tasks: planTasks.
|
|
30098
|
-
|
|
30449
|
+
tasks: planTasks.map(taskSummary).filter((item) => Boolean(item)),
|
|
30450
|
+
total_tasks: totalPlanTasks,
|
|
30451
|
+
omitted_tasks: Math.max(0, totalPlanTasks - planTasks.length)
|
|
30099
30452
|
} : null,
|
|
30100
30453
|
acceptance_criteria: acceptanceCriteria(task, limit.max_text_chars),
|
|
30101
30454
|
dependencies: {
|
|
@@ -30205,6 +30558,8 @@ ${pack.task.description}` : null,
|
|
|
30205
30558
|
"## Project And Plan",
|
|
30206
30559
|
pack.project ? `- Project: ${pack.project.name} (${pack.project.path})` : "- Project: none",
|
|
30207
30560
|
pack.plan ? `- Plan: ${pack.plan.name} (${pack.plan.status})` : "- Plan: none",
|
|
30561
|
+
pack.plan ? `- Plan tasks: ${pack.plan.tasks.length} shown of ${pack.plan.total_tasks} total (${pack.plan.omitted_tasks} omitted)` : null,
|
|
30562
|
+
pack.plan && pack.plan.omitted_tasks > 0 ? `- Load more on demand: \`todos plans --show ${pack.plan.id}\`; inspect one task with \`todos show <task-id>\` or \`todos context-pack <task-id>\`.` : null,
|
|
30208
30563
|
pack.plan && pack.plan.tasks.length > 0 ? bullet(pack.plan.tasks.map((task) => `${task.status} ${task.short_id || task.id.slice(0, 8)} ${task.title}`)) : null,
|
|
30209
30564
|
"",
|
|
30210
30565
|
"## Acceptance Criteria",
|
|
@@ -30285,7 +30640,7 @@ function renderAgentContextPack(pack, format, compact2 = false) {
|
|
|
30285
30640
|
return compact2 ? renderAgentContextPackCompactMarkdown(pack) : renderAgentContextPackMarkdown(pack);
|
|
30286
30641
|
return compact2 ? JSON.stringify(pack) : JSON.stringify(pack, null, 2);
|
|
30287
30642
|
}
|
|
30288
|
-
var DEFAULT_LIMITS, ALL_CONTEXT_SECTIONS, BUDGET_TRIM_ORDER;
|
|
30643
|
+
var DEFAULT_LIMITS, MAX_CONTEXT_TASKS = 24, ALL_CONTEXT_SECTIONS, BUDGET_TRIM_ORDER;
|
|
30289
30644
|
var init_context_packs = __esm(() => {
|
|
30290
30645
|
init_comments();
|
|
30291
30646
|
init_database();
|
|
@@ -30304,7 +30659,7 @@ var init_context_packs = __esm(() => {
|
|
|
30304
30659
|
verification_limit: 10,
|
|
30305
30660
|
run_limit: 3,
|
|
30306
30661
|
dependency_limit: 12,
|
|
30307
|
-
plan_task_limit:
|
|
30662
|
+
plan_task_limit: 24,
|
|
30308
30663
|
max_text_chars: 6000,
|
|
30309
30664
|
summary_char_limit: 480,
|
|
30310
30665
|
stale_after_hours: 72
|
|
@@ -31353,7 +31708,7 @@ function parseObject2(value) {
|
|
|
31353
31708
|
function normalizeKey(value) {
|
|
31354
31709
|
return value.trim().toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
31355
31710
|
}
|
|
31356
|
-
function
|
|
31711
|
+
function normalizeTaskFindingFingerprint(value) {
|
|
31357
31712
|
const normalized = normalizeKey(value);
|
|
31358
31713
|
if (!normalized)
|
|
31359
31714
|
throw new Error("finding fingerprint is required");
|
|
@@ -31462,7 +31817,7 @@ function assertTask(taskId, db) {
|
|
|
31462
31817
|
throw new TaskNotFoundError(taskId);
|
|
31463
31818
|
}
|
|
31464
31819
|
function nextFinding(input, db) {
|
|
31465
|
-
const fingerprint2 =
|
|
31820
|
+
const fingerprint2 = normalizeTaskFindingFingerprint(input.fingerprint);
|
|
31466
31821
|
const title = redactOptional(input.title, 300);
|
|
31467
31822
|
if (!title)
|
|
31468
31823
|
throw new Error("finding title is required");
|
|
@@ -31610,7 +31965,7 @@ function resolveMissingTaskFindings(input, db) {
|
|
|
31610
31965
|
const timestamp4 = input.resolved_at || now();
|
|
31611
31966
|
const status = normalizeResolutionStatus(input.status);
|
|
31612
31967
|
const runId = resolveRunForTask(input.run_id, input.task_id, d);
|
|
31613
|
-
const present = new Set(input.fingerprints.map(
|
|
31968
|
+
const present = new Set(input.fingerprints.map(normalizeTaskFindingFingerprint));
|
|
31614
31969
|
const warnings = [];
|
|
31615
31970
|
const conditions = ["task_id = ?", "status = 'open'"];
|
|
31616
31971
|
const params = [input.task_id];
|
|
@@ -34198,7 +34553,7 @@ var package_default;
|
|
|
34198
34553
|
var init_package = __esm(() => {
|
|
34199
34554
|
package_default = {
|
|
34200
34555
|
name: "@hasna/todos",
|
|
34201
|
-
version: "0.13.
|
|
34556
|
+
version: "0.13.3",
|
|
34202
34557
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
34203
34558
|
type: "module",
|
|
34204
34559
|
main: "dist/index.js",
|
|
@@ -34232,6 +34587,10 @@ var init_package = __esm(() => {
|
|
|
34232
34587
|
"./storage": {
|
|
34233
34588
|
types: "./dist/storage.d.ts",
|
|
34234
34589
|
import: "./dist/storage.js"
|
|
34590
|
+
},
|
|
34591
|
+
"./testing": {
|
|
34592
|
+
types: "./dist/testing.d.ts",
|
|
34593
|
+
import: "./dist/testing.js"
|
|
34235
34594
|
}
|
|
34236
34595
|
},
|
|
34237
34596
|
workspaces: [
|
|
@@ -34244,8 +34603,8 @@ var init_package = __esm(() => {
|
|
|
34244
34603
|
"README.md"
|
|
34245
34604
|
],
|
|
34246
34605
|
scripts: {
|
|
34247
|
-
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 --external '@hasna/contracts' --external '@hasna/contracts/*' && 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 --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
|
|
34248
|
-
"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 --external '@hasna/contracts' --external '@hasna/contracts/*' && 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 --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*'",
|
|
34606
|
+
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 --external '@hasna/contracts' --external '@hasna/contracts/*' && 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 --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*' && tsc --emitDeclarationOnly --outDir dist",
|
|
34607
|
+
"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 --external '@hasna/contracts' --external '@hasna/contracts/*' && 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 --outdir dist --target bun --external '@hasna/contracts' --external '@hasna/contracts/*'",
|
|
34249
34608
|
migrate: "bun run src/server/index.ts migrate",
|
|
34250
34609
|
"backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
|
|
34251
34610
|
"generate:sdk": "bun run scripts/generate-sdk.ts",
|
|
@@ -43951,15 +44310,15 @@ function resolveTaskRefLocal(db, ref) {
|
|
|
43951
44310
|
return null;
|
|
43952
44311
|
if (TASK_UUID_RE.test(raw))
|
|
43953
44312
|
return getTask(raw, db);
|
|
43954
|
-
const prefixRows = db.query("SELECT id FROM tasks WHERE LOWER(id) LIKE ? ESCAPE '\\' LIMIT 2").all(`${raw.replace(/[\\%_]/g, (c) => `\\${c}`)}%`);
|
|
44313
|
+
const prefixRows = db.query("SELECT id, project_id FROM tasks WHERE LOWER(id) LIKE ? ESCAPE '\\' ORDER BY project_id, id LIMIT 2").all(`${raw.replace(/[\\%_]/g, (c) => `\\${c}`)}%`);
|
|
43955
44314
|
if (prefixRows.length > 1) {
|
|
43956
|
-
throw new
|
|
44315
|
+
throw new TaskReferenceAmbiguousError(ref, prefixRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
|
|
43957
44316
|
}
|
|
43958
44317
|
if (prefixRows.length === 1)
|
|
43959
44318
|
return getTask(prefixRows[0].id, db);
|
|
43960
|
-
const shortIdRows = db.query("SELECT id FROM tasks WHERE LOWER(short_id) = ? LIMIT 2").all(raw);
|
|
44319
|
+
const shortIdRows = db.query("SELECT id, project_id FROM tasks WHERE LOWER(short_id) = ? ORDER BY project_id, id LIMIT 2").all(raw);
|
|
43961
44320
|
if (shortIdRows.length > 1) {
|
|
43962
|
-
throw new
|
|
44321
|
+
throw new TaskReferenceAmbiguousError(ref, shortIdRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
|
|
43963
44322
|
}
|
|
43964
44323
|
if (shortIdRows.length === 1)
|
|
43965
44324
|
return getTask(shortIdRows[0].id, db);
|
|
@@ -44108,6 +44467,9 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
44108
44467
|
exportSnapshot: () => exportSqliteTodosStorageSnapshot(database()),
|
|
44109
44468
|
importSnapshot: (snapshot) => importSqliteTodosStorageSnapshot(snapshot, database())
|
|
44110
44469
|
},
|
|
44470
|
+
integrity: {
|
|
44471
|
+
report: () => scanSqliteIntegrity(database())
|
|
44472
|
+
},
|
|
44111
44473
|
transaction: (fn) => {
|
|
44112
44474
|
const tx = database().transaction(() => fn(adapter));
|
|
44113
44475
|
return tx();
|
|
@@ -44117,6 +44479,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
44117
44479
|
}
|
|
44118
44480
|
var TASK_UUID_RE;
|
|
44119
44481
|
var init_local_sqlite = __esm(() => {
|
|
44482
|
+
init_types();
|
|
44120
44483
|
init_search();
|
|
44121
44484
|
init_tasks();
|
|
44122
44485
|
init_projects();
|
|
@@ -44127,6 +44490,7 @@ var init_local_sqlite = __esm(() => {
|
|
|
44127
44490
|
init_audit();
|
|
44128
44491
|
init_comments();
|
|
44129
44492
|
init_database();
|
|
44493
|
+
init_integrity2();
|
|
44130
44494
|
init_sqlite_snapshot();
|
|
44131
44495
|
TASK_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
44132
44496
|
});
|
|
@@ -45051,6 +45415,9 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
45051
45415
|
exportSnapshot: () => exportSnapshot(store),
|
|
45052
45416
|
importSnapshot: (snapshot, context) => importSnapshot(snapshot, store, context)
|
|
45053
45417
|
},
|
|
45418
|
+
integrity: {
|
|
45419
|
+
report: () => store.integrityReport()
|
|
45420
|
+
},
|
|
45054
45421
|
transaction: (fn) => fn(adapter)
|
|
45055
45422
|
};
|
|
45056
45423
|
return adapter;
|
|
@@ -45157,8 +45524,9 @@ class PostgresJsonRecordStore {
|
|
|
45157
45524
|
conds.push(`payload->>'agent_id' = ${p(filter.agent_id)}`);
|
|
45158
45525
|
if (filter.session_id !== undefined)
|
|
45159
45526
|
conds.push(`payload->>'session_id' = ${p(filter.session_id)}`);
|
|
45160
|
-
if (filter.tags?.length)
|
|
45161
|
-
conds.push(`payload->'tags'
|
|
45527
|
+
if (filter.tags?.length) {
|
|
45528
|
+
conds.push(`EXISTS (SELECT 1 FROM jsonb_array_elements_text(COALESCE(payload->'tags', '[]'::jsonb)) AS task_tags(tag) ` + `WHERE ${inClause("task_tags.tag", filter.tags)})`);
|
|
45529
|
+
}
|
|
45162
45530
|
if (filter.has_recurrence !== undefined) {
|
|
45163
45531
|
conds.push(`(COALESCE(payload->>'recurrence_rule', '') <> '') = ${p(filter.has_recurrence)}`);
|
|
45164
45532
|
}
|
|
@@ -45219,7 +45587,8 @@ class PostgresJsonRecordStore {
|
|
|
45219
45587
|
AND object_id COLLATE "C" >= $2 AND object_id COLLATE "C" < $3
|
|
45220
45588
|
LIMIT 2`, [this.service, raw, upper]);
|
|
45221
45589
|
if (prefixResult.rows.length > 1) {
|
|
45222
|
-
|
|
45590
|
+
const candidates = prefixResult.rows.map((row) => payloadRecord2(row.payload));
|
|
45591
|
+
throw new TaskReferenceAmbiguousError(ref, candidates.map((task2) => ({ task_id: task2.id, project_id: task2.project_id })));
|
|
45223
45592
|
}
|
|
45224
45593
|
if (prefixResult.rows.length === 1) {
|
|
45225
45594
|
return payloadRecord2(prefixResult.rows[0].payload);
|
|
@@ -45230,7 +45599,8 @@ class PostgresJsonRecordStore {
|
|
|
45230
45599
|
AND LOWER(payload->>'short_id') = $2
|
|
45231
45600
|
LIMIT 2`, [this.service, raw]);
|
|
45232
45601
|
if (shortIdResult.rows.length > 1) {
|
|
45233
|
-
|
|
45602
|
+
const candidates = shortIdResult.rows.map((row) => payloadRecord2(row.payload));
|
|
45603
|
+
throw new TaskReferenceAmbiguousError(ref, candidates.map((task2) => ({ task_id: task2.id, project_id: task2.project_id })));
|
|
45234
45604
|
}
|
|
45235
45605
|
if (shortIdResult.rows.length === 1) {
|
|
45236
45606
|
return payloadRecord2(shortIdResult.rows[0].payload);
|
|
@@ -45244,6 +45614,24 @@ class PostgresJsonRecordStore {
|
|
|
45244
45614
|
const result = await this.options.client.query(sql, params);
|
|
45245
45615
|
return Number(result.rows[0]?.count ?? 0);
|
|
45246
45616
|
}
|
|
45617
|
+
async integrityReport() {
|
|
45618
|
+
await this.ensureSchema();
|
|
45619
|
+
const conditions = [];
|
|
45620
|
+
for (const spec of INTEGRITY_CONDITIONS) {
|
|
45621
|
+
const { sql, params } = buildPostgresIntegritySql(spec, { table: this.tableName, service: this.service });
|
|
45622
|
+
try {
|
|
45623
|
+
const result = await this.options.client.query(sql, params);
|
|
45624
|
+
const row = result.rows[0];
|
|
45625
|
+
conditions.push(measuredCondition(spec, {
|
|
45626
|
+
count: Number(row?.count ?? 0),
|
|
45627
|
+
open_count: spec.entity === "task" ? Number(row?.open_count ?? 0) : null
|
|
45628
|
+
}, "postgres"));
|
|
45629
|
+
} catch (error) {
|
|
45630
|
+
conditions.push(unverifiedCondition(spec, error instanceof Error ? error.message : String(error)));
|
|
45631
|
+
}
|
|
45632
|
+
}
|
|
45633
|
+
return buildIntegrityReport(conditions, new Date().toISOString());
|
|
45634
|
+
}
|
|
45247
45635
|
async listTombstones() {
|
|
45248
45636
|
await this.ensureSchema();
|
|
45249
45637
|
const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
|
|
@@ -45719,7 +46107,7 @@ async function startTask2(id, agentId, store) {
|
|
|
45719
46107
|
if (task2.status !== "pending" && task2.status !== "in_progress") {
|
|
45720
46108
|
throw new Error(`Task is ${task2.status} and cannot be started by ${agentId}`);
|
|
45721
46109
|
}
|
|
45722
|
-
|
|
46110
|
+
const started = await patchTask(task2, {
|
|
45723
46111
|
status: "in_progress",
|
|
45724
46112
|
assigned_to: task2.assigned_to ?? agentId,
|
|
45725
46113
|
agent_id: task2.agent_id ?? agentId,
|
|
@@ -45727,6 +46115,8 @@ async function startTask2(id, agentId, store) {
|
|
|
45727
46115
|
locked_at: new Date().toISOString(),
|
|
45728
46116
|
started_at: task2.started_at ?? new Date().toISOString()
|
|
45729
46117
|
}, store);
|
|
46118
|
+
await logTaskChange2(task2.id, "start", "status", task2.status, "in_progress", agentId, store);
|
|
46119
|
+
return started;
|
|
45730
46120
|
}
|
|
45731
46121
|
async function completeTask2(id, agentId, options, store) {
|
|
45732
46122
|
const task2 = await store.completeTask(id, agentId, options);
|
|
@@ -46393,6 +46783,7 @@ var CLOUD_LOCK_EXPIRY_MINUTES = 30, TASK_ORDER_TIEBREAK = "CASE payload->>'prior
|
|
|
46393
46783
|
var init_postgres_adapter = __esm(() => {
|
|
46394
46784
|
init_types();
|
|
46395
46785
|
init_postgres_sync();
|
|
46786
|
+
init_integrity();
|
|
46396
46787
|
init_redaction();
|
|
46397
46788
|
TASK_ORDER_BY = `ORDER BY ${TASK_ORDER_TIEBREAK}`;
|
|
46398
46789
|
});
|
|
@@ -46981,6 +47372,7 @@ __export(exports_cloud, {
|
|
|
46981
47372
|
resolveCloudDatabaseUrl: () => resolveCloudDatabaseUrl,
|
|
46982
47373
|
pingCloud: () => pingCloud,
|
|
46983
47374
|
normalizeCloudPayloads: () => normalizeCloudPayloads,
|
|
47375
|
+
isPostgresBackendConfigured: () => isPostgresBackendConfigured,
|
|
46984
47376
|
isCloudModeEnabled: () => isCloudModeEnabled,
|
|
46985
47377
|
getCloudVerifier: () => getCloudVerifier,
|
|
46986
47378
|
getCloudStorageAdapter: () => getCloudStorageAdapter,
|
|
@@ -47003,9 +47395,12 @@ function resolveCloudDatabaseUrl(env = process.env) {
|
|
|
47003
47395
|
function resolveSigningSecret(env = process.env) {
|
|
47004
47396
|
return env.HASNA_TODOS_API_SIGNING_KEY || env.HASNA_API_SIGNING_KEY || env.API_KEY_SIGNING_SECRET || undefined;
|
|
47005
47397
|
}
|
|
47006
|
-
function
|
|
47398
|
+
function isPostgresBackendConfigured(env = process.env) {
|
|
47007
47399
|
return Boolean(resolveCloudDatabaseUrl(env));
|
|
47008
47400
|
}
|
|
47401
|
+
function isCloudModeEnabled(env = process.env) {
|
|
47402
|
+
return isPostgresBackendConfigured(env);
|
|
47403
|
+
}
|
|
47009
47404
|
function getClient() {
|
|
47010
47405
|
if (cachedClient)
|
|
47011
47406
|
return cachedClient;
|
|
@@ -47185,7 +47580,7 @@ function authNotConfiguredMessage(host) {
|
|
|
47185
47580
|
`);
|
|
47186
47581
|
}
|
|
47187
47582
|
function resolveAuthPosture(input) {
|
|
47188
|
-
const hosted = input.hosted ??
|
|
47583
|
+
const hosted = input.hosted ?? isPostgresBackendConfigured();
|
|
47189
47584
|
const hasCredentialSource = Boolean(input.apiKey) || input.hasGeneratedKeys;
|
|
47190
47585
|
if (hasCredentialSource) {
|
|
47191
47586
|
return {
|
|
@@ -49146,6 +49541,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
49146
49541
|
{ name: "task_list_id", in: "query", schema: { type: "string" } },
|
|
49147
49542
|
{ name: "assigned_to", in: "query", schema: { type: "string" } },
|
|
49148
49543
|
{ name: "agent_id", in: "query", schema: { type: "string" } },
|
|
49544
|
+
{ name: "tags", in: "query", schema: { type: "string" }, description: "Comma-separated tags; matches tasks carrying any of them" },
|
|
49149
49545
|
{ name: "limit", in: "query", schema: { type: "integer", minimum: 1 } },
|
|
49150
49546
|
{ name: "offset", in: "query", schema: { type: "integer", minimum: 0 } }
|
|
49151
49547
|
],
|
|
@@ -50350,6 +50746,9 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
50350
50746
|
...url.searchParams.get("task_list_id") ? { task_list_id: url.searchParams.get("task_list_id") } : {},
|
|
50351
50747
|
...url.searchParams.get("assigned_to") ? { assigned_to: url.searchParams.get("assigned_to") } : {},
|
|
50352
50748
|
...url.searchParams.get("agent_id") ? { agent_id: url.searchParams.get("agent_id") } : {},
|
|
50749
|
+
...url.searchParams.get("tags") ? {
|
|
50750
|
+
tags: url.searchParams.get("tags").split(",").map((tag) => tag.trim()).filter(Boolean)
|
|
50751
|
+
} : {},
|
|
50353
50752
|
...url.searchParams.get("limit") ? { limit: Number(url.searchParams.get("limit")) } : {},
|
|
50354
50753
|
...url.searchParams.get("offset") ? { offset: Number(url.searchParams.get("offset")) } : {}
|
|
50355
50754
|
};
|
|
@@ -50631,6 +51030,13 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
50631
51030
|
try {
|
|
50632
51031
|
task2 = await store.tasks.resolveRef(id);
|
|
50633
51032
|
} catch (e) {
|
|
51033
|
+
if (e instanceof TaskReferenceAmbiguousError) {
|
|
51034
|
+
return error(409, e.message, {
|
|
51035
|
+
code: TaskReferenceAmbiguousError.code,
|
|
51036
|
+
candidate_project_ids: e.candidateProjectIds,
|
|
51037
|
+
candidate_task_ids: e.candidateTaskIds
|
|
51038
|
+
});
|
|
51039
|
+
}
|
|
50634
51040
|
const msg = e.message || "";
|
|
50635
51041
|
if (/ambiguous/i.test(msg))
|
|
50636
51042
|
return error(409, msg);
|
|
@@ -50988,6 +51394,15 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
50988
51394
|
]);
|
|
50989
51395
|
return json3({ tasks, tasks_all: tasksAll, subtasks: tasksAll - tasks, projects: projects.length });
|
|
50990
51396
|
}
|
|
51397
|
+
if (resource === "integrity" && !id) {
|
|
51398
|
+
if (method !== "GET")
|
|
51399
|
+
return error(405, `method ${method} not allowed on /v1/integrity`);
|
|
51400
|
+
if (typeof store.integrity?.report !== "function") {
|
|
51401
|
+
return error(501, "referential-integrity reporting is not supported by this storage backend");
|
|
51402
|
+
}
|
|
51403
|
+
const integrity = await store.integrity.report();
|
|
51404
|
+
return json3({ integrity });
|
|
51405
|
+
}
|
|
50991
51406
|
if (resource === "import") {
|
|
50992
51407
|
if (method !== "POST")
|
|
50993
51408
|
return error(405, `method ${method} not allowed on /v1/import`);
|
|
@@ -51007,6 +51422,13 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51007
51422
|
}
|
|
51008
51423
|
return error(404, `unknown /v1 resource: ${resource ?? "(root)"}`);
|
|
51009
51424
|
} catch (e) {
|
|
51425
|
+
if (e instanceof TaskReferenceAmbiguousError) {
|
|
51426
|
+
return error(409, e.message, {
|
|
51427
|
+
code: TaskReferenceAmbiguousError.code,
|
|
51428
|
+
candidate_project_ids: e.candidateProjectIds,
|
|
51429
|
+
candidate_task_ids: e.candidateTaskIds
|
|
51430
|
+
});
|
|
51431
|
+
}
|
|
51010
51432
|
if (e instanceof LockError)
|
|
51011
51433
|
return error(409, e.message, { code: LockError.code });
|
|
51012
51434
|
if (e instanceof ResourceConflictError)
|
|
@@ -51527,23 +51949,24 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
51527
51949
|
}
|
|
51528
51950
|
if ((path === "/health" || path === "/ready" || path === "/version") && method === "GET") {
|
|
51529
51951
|
const { getPackageVersion: getPackageVersion2 } = await Promise.resolve().then(() => (init_package_version(), exports_package_version));
|
|
51530
|
-
const {
|
|
51531
|
-
const
|
|
51952
|
+
const { isPostgresBackendConfigured: isPostgresBackendConfigured2, pingCloud: pingCloud2 } = await Promise.resolve().then(() => (init_cloud(), exports_cloud));
|
|
51953
|
+
const backend = isPostgresBackendConfigured2() ? "postgresql" : "sqlite";
|
|
51954
|
+
const mode = backend === "postgresql" ? "remote" : "local";
|
|
51532
51955
|
const version = getPackageVersion2();
|
|
51533
51956
|
if (path === "/version") {
|
|
51534
|
-
return Response.json({ status: "ok", version, mode, name: "todos" });
|
|
51957
|
+
return Response.json({ status: "ok", version, mode, backend, name: "todos" });
|
|
51535
51958
|
}
|
|
51536
51959
|
if (path === "/ready") {
|
|
51537
|
-
if (
|
|
51960
|
+
if (backend === "postgresql") {
|
|
51538
51961
|
try {
|
|
51539
51962
|
await pingCloud2();
|
|
51540
51963
|
} catch (e) {
|
|
51541
|
-
return Response.json({ status: "unavailable", version, mode, error: e.message }, { status: 503 });
|
|
51964
|
+
return Response.json({ status: "unavailable", version, mode, backend, error: e.message }, { status: 503 });
|
|
51542
51965
|
}
|
|
51543
51966
|
}
|
|
51544
|
-
return Response.json({ status: "ready", version, mode });
|
|
51967
|
+
return Response.json({ status: "ready", version, mode, backend });
|
|
51545
51968
|
}
|
|
51546
|
-
return Response.json({ status: "ok", version, mode, name: "todos" });
|
|
51969
|
+
return Response.json({ status: "ok", version, mode, backend, name: "todos" });
|
|
51547
51970
|
}
|
|
51548
51971
|
if ((path === "/openapi.json" || path === "/v1/openapi.json") && method === "GET") {
|
|
51549
51972
|
const { buildV1OpenApiDocument: buildV1OpenApiDocument2 } = await Promise.resolve().then(() => (init_openapi(), exports_openapi));
|
|
@@ -51899,6 +52322,15 @@ function formatError(error2) {
|
|
|
51899
52322
|
if (error2 instanceof TaskNotFoundError) {
|
|
51900
52323
|
return JSON.stringify({ code: TaskNotFoundError.code, message: error2.message, suggestion: TaskNotFoundError.suggestion });
|
|
51901
52324
|
}
|
|
52325
|
+
if (error2 instanceof TaskReferenceAmbiguousError) {
|
|
52326
|
+
return JSON.stringify({
|
|
52327
|
+
code: TaskReferenceAmbiguousError.code,
|
|
52328
|
+
message: error2.message,
|
|
52329
|
+
candidate_project_ids: error2.candidateProjectIds,
|
|
52330
|
+
candidate_task_ids: error2.candidateTaskIds,
|
|
52331
|
+
suggestion: "Use a full task UUID."
|
|
52332
|
+
});
|
|
52333
|
+
}
|
|
51902
52334
|
if (error2 instanceof ProjectNotFoundError) {
|
|
51903
52335
|
return JSON.stringify({ code: ProjectNotFoundError.code, message: error2.message, suggestion: ProjectNotFoundError.suggestion });
|
|
51904
52336
|
}
|