@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/server/index.js
CHANGED
|
@@ -70,7 +70,7 @@ var package_default;
|
|
|
70
70
|
var init_package = __esm(() => {
|
|
71
71
|
package_default = {
|
|
72
72
|
name: "@hasna/todos",
|
|
73
|
-
version: "0.13.
|
|
73
|
+
version: "0.13.3",
|
|
74
74
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
75
75
|
type: "module",
|
|
76
76
|
main: "dist/index.js",
|
|
@@ -104,6 +104,10 @@ var init_package = __esm(() => {
|
|
|
104
104
|
"./storage": {
|
|
105
105
|
types: "./dist/storage.d.ts",
|
|
106
106
|
import: "./dist/storage.js"
|
|
107
|
+
},
|
|
108
|
+
"./testing": {
|
|
109
|
+
types: "./dist/testing.d.ts",
|
|
110
|
+
import: "./dist/testing.js"
|
|
107
111
|
}
|
|
108
112
|
},
|
|
109
113
|
workspaces: [
|
|
@@ -116,8 +120,8 @@ var init_package = __esm(() => {
|
|
|
116
120
|
"README.md"
|
|
117
121
|
],
|
|
118
122
|
scripts: {
|
|
119
|
-
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",
|
|
120
|
-
"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/*'",
|
|
123
|
+
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",
|
|
124
|
+
"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/*'",
|
|
121
125
|
migrate: "bun run src/server/index.ts migrate",
|
|
122
126
|
"backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
|
|
123
127
|
"generate:sdk": "bun run scripts/generate-sdk.ts",
|
|
@@ -201,15 +205,18 @@ var init_package_version = __esm(() => {
|
|
|
201
205
|
var exports_config = {};
|
|
202
206
|
__export(exports_config, {
|
|
203
207
|
parseStorageMode: () => parseStorageMode,
|
|
208
|
+
parseStorageBackend: () => parseStorageBackend,
|
|
204
209
|
loadTodosStorageConfig: () => loadTodosStorageConfig,
|
|
205
210
|
loadStorageConfig: () => loadStorageConfig,
|
|
206
211
|
isTodosShadowEnabled: () => isTodosShadowEnabled,
|
|
207
212
|
isTodosRemoteStorageEnabled: () => isTodosRemoteStorageEnabled,
|
|
213
|
+
isTodosPostgresBackend: () => isTodosPostgresBackend,
|
|
208
214
|
getTodosStorageShadowEnvName: () => getTodosStorageShadowEnvName,
|
|
209
215
|
getTodosStorageMode: () => getTodosStorageMode,
|
|
210
216
|
getTodosStorageEnvName: () => getTodosStorageEnvName,
|
|
211
217
|
getTodosStorageDatabaseUrl: () => getTodosStorageDatabaseUrl,
|
|
212
218
|
getTodosStorageDatabaseEnv: () => getTodosStorageDatabaseEnv,
|
|
219
|
+
getTodosStorageBackend: () => getTodosStorageBackend,
|
|
213
220
|
getStorageMode: () => getStorageMode,
|
|
214
221
|
getStorageDatabaseUrl: () => getStorageDatabaseUrl,
|
|
215
222
|
getStorageDatabaseEnv: () => getStorageDatabaseEnv,
|
|
@@ -234,7 +241,7 @@ function getCanonicalTodosRdsConfig(env = process.env) {
|
|
|
234
241
|
};
|
|
235
242
|
}
|
|
236
243
|
function loadTodosStorageConfig(env = process.env) {
|
|
237
|
-
const mode =
|
|
244
|
+
const mode = getTodosStorageBackend(env);
|
|
238
245
|
const databaseUrl = getTodosStorageDatabaseUrl(env);
|
|
239
246
|
const bucket = readStorageEnv(env, "s3Bucket").value;
|
|
240
247
|
const prefix = readStorageEnv(env, "s3Prefix").value ?? "todos/";
|
|
@@ -271,29 +278,41 @@ function loadTodosStorageConfig(env = process.env) {
|
|
|
271
278
|
function loadStorageConfig(env = process.env) {
|
|
272
279
|
return loadTodosStorageConfig(env);
|
|
273
280
|
}
|
|
281
|
+
function isTodosPostgresBackend(config) {
|
|
282
|
+
return config.mode === "postgres";
|
|
283
|
+
}
|
|
274
284
|
function isTodosRemoteStorageEnabled(config) {
|
|
275
|
-
return config
|
|
285
|
+
return isTodosPostgresBackend(config);
|
|
276
286
|
}
|
|
277
287
|
function assertTodosRemoteStorageConfig(config) {
|
|
278
|
-
if (!
|
|
288
|
+
if (!isTodosPostgresBackend(config))
|
|
279
289
|
return;
|
|
280
290
|
if (!config.database?.url) {
|
|
281
291
|
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${TODOS_STORAGE_ENV.mode}=${config.mode}`);
|
|
282
292
|
}
|
|
283
293
|
}
|
|
284
|
-
function
|
|
294
|
+
function parseStorageBackend(value) {
|
|
285
295
|
const normalized = clean(value)?.toLowerCase();
|
|
286
296
|
if (!normalized)
|
|
287
|
-
return "
|
|
288
|
-
if (normalized === "
|
|
297
|
+
return "sqlite";
|
|
298
|
+
if (normalized === "sqlite" || normalized === "postgres")
|
|
289
299
|
return normalized;
|
|
290
|
-
|
|
300
|
+
const legacy = LEGACY_BACKEND_TOKENS[normalized];
|
|
301
|
+
if (legacy)
|
|
302
|
+
return legacy;
|
|
303
|
+
throw new Error(`${TODOS_STORAGE_ENV.mode} must be sqlite or postgres (legacy values local and remote are accepted)`);
|
|
304
|
+
}
|
|
305
|
+
function parseStorageMode(value) {
|
|
306
|
+
return parseStorageBackend(value);
|
|
307
|
+
}
|
|
308
|
+
function getTodosStorageBackend(env = process.env) {
|
|
309
|
+
return parseStorageBackend(readStorageEnv(env, "mode").value);
|
|
291
310
|
}
|
|
292
311
|
function getTodosStorageMode(env = process.env) {
|
|
293
|
-
return
|
|
312
|
+
return getTodosStorageBackend(env);
|
|
294
313
|
}
|
|
295
314
|
function getStorageMode(env = process.env) {
|
|
296
|
-
return
|
|
315
|
+
return getTodosStorageBackend(env);
|
|
297
316
|
}
|
|
298
317
|
function isTodosShadowEnabled(env = process.env) {
|
|
299
318
|
return parseBoolean(readStorageEnv(env, "shadow").value, false);
|
|
@@ -304,8 +323,8 @@ function getTodosStorageShadowEnvName(env = process.env) {
|
|
|
304
323
|
function assertTodosShadowConfig(config, env = process.env) {
|
|
305
324
|
if (!isTodosShadowEnabled(env))
|
|
306
325
|
return;
|
|
307
|
-
if (config.mode !== "
|
|
308
|
-
throw new Error(`${readStorageEnv(env, "shadow").name} shadow mirror requires
|
|
326
|
+
if (config.mode !== "sqlite") {
|
|
327
|
+
throw new Error(`${readStorageEnv(env, "shadow").name} shadow mirror requires the sqlite backend (got ${config.mode})`);
|
|
309
328
|
}
|
|
310
329
|
if (!config.database?.url) {
|
|
311
330
|
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${readStorageEnv(env, "shadow").name} is enabled`);
|
|
@@ -361,7 +380,7 @@ function parsePositiveInteger(value, fallback) {
|
|
|
361
380
|
}
|
|
362
381
|
return parsed;
|
|
363
382
|
}
|
|
364
|
-
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";
|
|
383
|
+
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;
|
|
365
384
|
var init_config = __esm(() => {
|
|
366
385
|
TODOS_STORAGE_TABLES = [
|
|
367
386
|
"todos_sync_records",
|
|
@@ -402,6 +421,14 @@ var init_config = __esm(() => {
|
|
|
402
421
|
syncBatchSize: "TODOS_SYNC_BATCH_SIZE",
|
|
403
422
|
syncDryRun: "TODOS_SYNC_DRY_RUN"
|
|
404
423
|
};
|
|
424
|
+
LEGACY_BACKEND_TOKENS = {
|
|
425
|
+
local: "sqlite",
|
|
426
|
+
remote: "postgres",
|
|
427
|
+
postgresql: "postgres",
|
|
428
|
+
hybrid: "postgres",
|
|
429
|
+
self_hosted: "postgres",
|
|
430
|
+
cloud: "postgres"
|
|
431
|
+
};
|
|
405
432
|
});
|
|
406
433
|
|
|
407
434
|
// src/storage/cloud-client.ts
|
|
@@ -469,7 +496,7 @@ var init_cloud_client = __esm(() => {
|
|
|
469
496
|
function isBlockingDependencyStatus(status) {
|
|
470
497
|
return status !== "completed" && status !== "cancelled";
|
|
471
498
|
}
|
|
472
|
-
var TASK_STATUSES, VersionConflictError, TaskNotFoundError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
|
|
499
|
+
var TASK_STATUSES, VersionConflictError, TaskNotFoundError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
|
|
473
500
|
var init_types = __esm(() => {
|
|
474
501
|
TASK_STATUSES = [
|
|
475
502
|
"pending",
|
|
@@ -502,6 +529,22 @@ var init_types = __esm(() => {
|
|
|
502
529
|
this.name = "TaskNotFoundError";
|
|
503
530
|
}
|
|
504
531
|
};
|
|
532
|
+
TaskReferenceAmbiguousError = class TaskReferenceAmbiguousError extends Error {
|
|
533
|
+
reference;
|
|
534
|
+
static code = "TASK_REFERENCE_AMBIGUOUS";
|
|
535
|
+
candidateTaskIds;
|
|
536
|
+
candidateProjectIds;
|
|
537
|
+
constructor(reference, candidates) {
|
|
538
|
+
const candidateTaskIds = [...new Set(candidates.map((candidate) => candidate.task_id))].sort();
|
|
539
|
+
const candidateProjectIds = [...new Set(candidates.map((candidate) => candidate.project_id).filter((projectId) => typeof projectId === "string" && projectId.length > 0))].sort();
|
|
540
|
+
const projectList = candidateProjectIds.length > 0 ? candidateProjectIds.join(", ") : "(unscoped)";
|
|
541
|
+
super(`Task reference is ambiguous: "${reference}". Candidate project IDs: ${projectList}. ` + "Use a full task UUID.");
|
|
542
|
+
this.reference = reference;
|
|
543
|
+
this.name = "TaskReferenceAmbiguousError";
|
|
544
|
+
this.candidateTaskIds = candidateTaskIds;
|
|
545
|
+
this.candidateProjectIds = candidateProjectIds;
|
|
546
|
+
}
|
|
547
|
+
};
|
|
505
548
|
ProjectNotFoundError = class ProjectNotFoundError extends Error {
|
|
506
549
|
projectId;
|
|
507
550
|
static code = "PROJECT_NOT_FOUND";
|
|
@@ -1086,6 +1129,185 @@ var init_postgres_sync = __esm(() => {
|
|
|
1086
1129
|
};
|
|
1087
1130
|
});
|
|
1088
1131
|
|
|
1132
|
+
// src/lib/integrity.ts
|
|
1133
|
+
function resolveIntegritySeverity(spec, measurement) {
|
|
1134
|
+
if (spec.kind === "dangling")
|
|
1135
|
+
return "error";
|
|
1136
|
+
if (spec.escalate_when_open && (measurement.open_count ?? 0) > 0)
|
|
1137
|
+
return "error";
|
|
1138
|
+
return spec.base_severity;
|
|
1139
|
+
}
|
|
1140
|
+
function plural(entity, count) {
|
|
1141
|
+
const [one, many] = ENTITY_LABEL[entity];
|
|
1142
|
+
return count === 1 ? one : many;
|
|
1143
|
+
}
|
|
1144
|
+
function formatIntegrityMessage(spec, measurement) {
|
|
1145
|
+
const noun = plural(spec.entity, measurement.count);
|
|
1146
|
+
const verb = measurement.count === 1 ? ["has", "references"] : ["have", "reference"];
|
|
1147
|
+
const open = measurement.open_count === null || measurement.open_count === 0 ? "" : ` (${measurement.open_count} still open)`;
|
|
1148
|
+
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}`;
|
|
1149
|
+
}
|
|
1150
|
+
function measuredCondition(spec, measurement, source) {
|
|
1151
|
+
return {
|
|
1152
|
+
id: spec.id,
|
|
1153
|
+
entity: spec.entity,
|
|
1154
|
+
field: spec.field,
|
|
1155
|
+
kind: spec.kind,
|
|
1156
|
+
count: measurement.count,
|
|
1157
|
+
open_count: measurement.open_count,
|
|
1158
|
+
severity: measurement.count > 0 ? resolveIntegritySeverity(spec, measurement) : null,
|
|
1159
|
+
verified: true,
|
|
1160
|
+
source,
|
|
1161
|
+
message: formatIntegrityMessage(spec, measurement),
|
|
1162
|
+
impact: spec.impact
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
function unverifiedCondition(spec, reason) {
|
|
1166
|
+
return {
|
|
1167
|
+
id: spec.id,
|
|
1168
|
+
entity: spec.entity,
|
|
1169
|
+
field: spec.field,
|
|
1170
|
+
kind: spec.kind,
|
|
1171
|
+
count: null,
|
|
1172
|
+
open_count: null,
|
|
1173
|
+
severity: null,
|
|
1174
|
+
verified: false,
|
|
1175
|
+
source: "unverified",
|
|
1176
|
+
unverified_reason: reason,
|
|
1177
|
+
message: `${spec.id}: NOT CHECKED \u2014 ${reason}`,
|
|
1178
|
+
impact: spec.impact
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
function summarizeIntegrity(conditions) {
|
|
1182
|
+
const measured = conditions.filter((condition) => condition.verified && condition.count !== null);
|
|
1183
|
+
const findings = measured.filter((condition) => (condition.count ?? 0) > 0);
|
|
1184
|
+
const unverified = conditions.length - measured.length;
|
|
1185
|
+
return {
|
|
1186
|
+
ok: unverified === 0 && findings.length === 0,
|
|
1187
|
+
findings: findings.length,
|
|
1188
|
+
rows: findings.reduce((total, condition) => total + (condition.count ?? 0), 0),
|
|
1189
|
+
errors: findings.filter((condition) => condition.severity === "error").length,
|
|
1190
|
+
warnings: findings.filter((condition) => condition.severity === "warn").length,
|
|
1191
|
+
unverified,
|
|
1192
|
+
complete: unverified === 0
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
function buildIntegrityReport(conditions, generatedAt) {
|
|
1196
|
+
const measuredSources = [...new Set(conditions.map((condition) => condition.source))].filter((source) => source !== "unverified");
|
|
1197
|
+
return {
|
|
1198
|
+
schema_version: TODOS_INTEGRITY_SCHEMA_VERSION,
|
|
1199
|
+
generated_at: generatedAt,
|
|
1200
|
+
source: measuredSources.length === 0 ? "unverified" : measuredSources.length === 1 ? measuredSources[0] : "remote-derived",
|
|
1201
|
+
conditions,
|
|
1202
|
+
summary: summarizeIntegrity(conditions)
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
function sqliteOpenStatusList() {
|
|
1206
|
+
return OPEN_TASK_STATUSES.map((status) => `'${status}'`).join(", ");
|
|
1207
|
+
}
|
|
1208
|
+
function buildSqliteIntegritySql(spec) {
|
|
1209
|
+
const table = SQLITE_TABLE[spec.entity];
|
|
1210
|
+
const target = SQLITE_TABLE[spec.target];
|
|
1211
|
+
const column = `t."${spec.field}"`;
|
|
1212
|
+
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})`;
|
|
1213
|
+
const openExpr = spec.entity === "task" ? `SUM(CASE WHEN t."status" IN (${sqliteOpenStatusList()}) THEN 1 ELSE 0 END)` : "NULL";
|
|
1214
|
+
return `SELECT COUNT(*) AS count, ${openExpr} AS open_count FROM "${table}" t WHERE ${predicate}`;
|
|
1215
|
+
}
|
|
1216
|
+
function buildPostgresIntegritySql(spec, options) {
|
|
1217
|
+
const params = [options.service, RECORD_TYPE[spec.entity]];
|
|
1218
|
+
const p = (value) => {
|
|
1219
|
+
params.push(value);
|
|
1220
|
+
return `$${params.length}`;
|
|
1221
|
+
};
|
|
1222
|
+
const column = `t.payload->>'${spec.field}'`;
|
|
1223
|
+
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})`;
|
|
1224
|
+
const openExpr = spec.entity === "task" ? `COUNT(*) FILTER (WHERE t.payload->>'status' IN (${OPEN_TASK_STATUSES.map((status) => p(status)).join(", ")}))::int` : "NULL::int";
|
|
1225
|
+
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}`;
|
|
1226
|
+
return { sql, params };
|
|
1227
|
+
}
|
|
1228
|
+
var TODOS_INTEGRITY_SCHEMA_VERSION = "todos.integrity.v1", TERMINAL_TASK_STATUSES, OPEN_TASK_STATUSES, INTEGRITY_CONDITIONS, ENTITY_LABEL, SQLITE_TABLE, RECORD_TYPE;
|
|
1229
|
+
var init_integrity = __esm(() => {
|
|
1230
|
+
init_types();
|
|
1231
|
+
TERMINAL_TASK_STATUSES = ["completed", "failed", "cancelled"];
|
|
1232
|
+
OPEN_TASK_STATUSES = TASK_STATUSES.filter((status) => !TERMINAL_TASK_STATUSES.includes(status));
|
|
1233
|
+
INTEGRITY_CONDITIONS = [
|
|
1234
|
+
{
|
|
1235
|
+
id: "tasks_without_project",
|
|
1236
|
+
entity: "task",
|
|
1237
|
+
field: "project_id",
|
|
1238
|
+
target: "project",
|
|
1239
|
+
kind: "missing",
|
|
1240
|
+
base_severity: "warn",
|
|
1241
|
+
escalate_when_open: true,
|
|
1242
|
+
impact: "invisible to every project-scoped read (list, status, next, claim)"
|
|
1243
|
+
},
|
|
1244
|
+
{
|
|
1245
|
+
id: "tasks_without_task_list",
|
|
1246
|
+
entity: "task",
|
|
1247
|
+
field: "task_list_id",
|
|
1248
|
+
target: "task_list",
|
|
1249
|
+
kind: "missing",
|
|
1250
|
+
base_severity: "warn",
|
|
1251
|
+
escalate_when_open: true,
|
|
1252
|
+
impact: "invisible to every task-list read \u2014 the list reports zero open work"
|
|
1253
|
+
},
|
|
1254
|
+
{
|
|
1255
|
+
id: "tasks_with_unregistered_project",
|
|
1256
|
+
entity: "task",
|
|
1257
|
+
field: "project_id",
|
|
1258
|
+
target: "project",
|
|
1259
|
+
kind: "dangling",
|
|
1260
|
+
base_severity: "error",
|
|
1261
|
+
escalate_when_open: false,
|
|
1262
|
+
impact: "points at a project id that does not exist; the reference can never resolve"
|
|
1263
|
+
},
|
|
1264
|
+
{
|
|
1265
|
+
id: "tasks_with_unregistered_task_list",
|
|
1266
|
+
entity: "task",
|
|
1267
|
+
field: "task_list_id",
|
|
1268
|
+
target: "task_list",
|
|
1269
|
+
kind: "dangling",
|
|
1270
|
+
base_severity: "error",
|
|
1271
|
+
escalate_when_open: false,
|
|
1272
|
+
impact: "points at a task-list id that does not exist; the reference can never resolve"
|
|
1273
|
+
},
|
|
1274
|
+
{
|
|
1275
|
+
id: "task_lists_without_project",
|
|
1276
|
+
entity: "task_list",
|
|
1277
|
+
field: "project_id",
|
|
1278
|
+
target: "project",
|
|
1279
|
+
kind: "missing",
|
|
1280
|
+
base_severity: "warn",
|
|
1281
|
+
escalate_when_open: false,
|
|
1282
|
+
impact: "unbound list \u2014 unreachable from any project, so its tasks are unroutable"
|
|
1283
|
+
},
|
|
1284
|
+
{
|
|
1285
|
+
id: "task_lists_with_unregistered_project",
|
|
1286
|
+
entity: "task_list",
|
|
1287
|
+
field: "project_id",
|
|
1288
|
+
target: "project",
|
|
1289
|
+
kind: "dangling",
|
|
1290
|
+
base_severity: "error",
|
|
1291
|
+
escalate_when_open: false,
|
|
1292
|
+
impact: "points at a project id that does not exist; the list can never be reached"
|
|
1293
|
+
}
|
|
1294
|
+
];
|
|
1295
|
+
ENTITY_LABEL = {
|
|
1296
|
+
task: ["task", "tasks"],
|
|
1297
|
+
task_list: ["task list", "task lists"]
|
|
1298
|
+
};
|
|
1299
|
+
SQLITE_TABLE = {
|
|
1300
|
+
task: "tasks",
|
|
1301
|
+
task_list: "task_lists",
|
|
1302
|
+
project: "projects"
|
|
1303
|
+
};
|
|
1304
|
+
RECORD_TYPE = {
|
|
1305
|
+
task: "tasks",
|
|
1306
|
+
task_list: "task_lists",
|
|
1307
|
+
project: "projects"
|
|
1308
|
+
};
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1089
1311
|
// src/lib/sync-utils.ts
|
|
1090
1312
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
|
|
1091
1313
|
import { join } from "path";
|
|
@@ -1425,6 +1647,9 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
1425
1647
|
exportSnapshot: () => exportSnapshot(store),
|
|
1426
1648
|
importSnapshot: (snapshot, context) => importSnapshot(snapshot, store, context)
|
|
1427
1649
|
},
|
|
1650
|
+
integrity: {
|
|
1651
|
+
report: () => store.integrityReport()
|
|
1652
|
+
},
|
|
1428
1653
|
transaction: (fn) => fn(adapter)
|
|
1429
1654
|
};
|
|
1430
1655
|
return adapter;
|
|
@@ -1531,8 +1756,9 @@ class PostgresJsonRecordStore {
|
|
|
1531
1756
|
conds.push(`payload->>'agent_id' = ${p(filter.agent_id)}`);
|
|
1532
1757
|
if (filter.session_id !== undefined)
|
|
1533
1758
|
conds.push(`payload->>'session_id' = ${p(filter.session_id)}`);
|
|
1534
|
-
if (filter.tags?.length)
|
|
1535
|
-
conds.push(`payload->'tags'
|
|
1759
|
+
if (filter.tags?.length) {
|
|
1760
|
+
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)})`);
|
|
1761
|
+
}
|
|
1536
1762
|
if (filter.has_recurrence !== undefined) {
|
|
1537
1763
|
conds.push(`(COALESCE(payload->>'recurrence_rule', '') <> '') = ${p(filter.has_recurrence)}`);
|
|
1538
1764
|
}
|
|
@@ -1593,7 +1819,8 @@ class PostgresJsonRecordStore {
|
|
|
1593
1819
|
AND object_id COLLATE "C" >= $2 AND object_id COLLATE "C" < $3
|
|
1594
1820
|
LIMIT 2`, [this.service, raw, upper]);
|
|
1595
1821
|
if (prefixResult.rows.length > 1) {
|
|
1596
|
-
|
|
1822
|
+
const candidates = prefixResult.rows.map((row) => payloadRecord2(row.payload));
|
|
1823
|
+
throw new TaskReferenceAmbiguousError(ref, candidates.map((task) => ({ task_id: task.id, project_id: task.project_id })));
|
|
1597
1824
|
}
|
|
1598
1825
|
if (prefixResult.rows.length === 1) {
|
|
1599
1826
|
return payloadRecord2(prefixResult.rows[0].payload);
|
|
@@ -1604,7 +1831,8 @@ class PostgresJsonRecordStore {
|
|
|
1604
1831
|
AND LOWER(payload->>'short_id') = $2
|
|
1605
1832
|
LIMIT 2`, [this.service, raw]);
|
|
1606
1833
|
if (shortIdResult.rows.length > 1) {
|
|
1607
|
-
|
|
1834
|
+
const candidates = shortIdResult.rows.map((row) => payloadRecord2(row.payload));
|
|
1835
|
+
throw new TaskReferenceAmbiguousError(ref, candidates.map((task) => ({ task_id: task.id, project_id: task.project_id })));
|
|
1608
1836
|
}
|
|
1609
1837
|
if (shortIdResult.rows.length === 1) {
|
|
1610
1838
|
return payloadRecord2(shortIdResult.rows[0].payload);
|
|
@@ -1618,6 +1846,24 @@ class PostgresJsonRecordStore {
|
|
|
1618
1846
|
const result = await this.options.client.query(sql, params);
|
|
1619
1847
|
return Number(result.rows[0]?.count ?? 0);
|
|
1620
1848
|
}
|
|
1849
|
+
async integrityReport() {
|
|
1850
|
+
await this.ensureSchema();
|
|
1851
|
+
const conditions = [];
|
|
1852
|
+
for (const spec of INTEGRITY_CONDITIONS) {
|
|
1853
|
+
const { sql, params } = buildPostgresIntegritySql(spec, { table: this.tableName, service: this.service });
|
|
1854
|
+
try {
|
|
1855
|
+
const result = await this.options.client.query(sql, params);
|
|
1856
|
+
const row = result.rows[0];
|
|
1857
|
+
conditions.push(measuredCondition(spec, {
|
|
1858
|
+
count: Number(row?.count ?? 0),
|
|
1859
|
+
open_count: spec.entity === "task" ? Number(row?.open_count ?? 0) : null
|
|
1860
|
+
}, "postgres"));
|
|
1861
|
+
} catch (error) {
|
|
1862
|
+
conditions.push(unverifiedCondition(spec, error instanceof Error ? error.message : String(error)));
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
return buildIntegrityReport(conditions, new Date().toISOString());
|
|
1866
|
+
}
|
|
1621
1867
|
async listTombstones() {
|
|
1622
1868
|
await this.ensureSchema();
|
|
1623
1869
|
const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
|
|
@@ -2093,7 +2339,7 @@ async function startTask(id, agentId, store) {
|
|
|
2093
2339
|
if (task.status !== "pending" && task.status !== "in_progress") {
|
|
2094
2340
|
throw new Error(`Task is ${task.status} and cannot be started by ${agentId}`);
|
|
2095
2341
|
}
|
|
2096
|
-
|
|
2342
|
+
const started = await patchTask(task, {
|
|
2097
2343
|
status: "in_progress",
|
|
2098
2344
|
assigned_to: task.assigned_to ?? agentId,
|
|
2099
2345
|
agent_id: task.agent_id ?? agentId,
|
|
@@ -2101,6 +2347,8 @@ async function startTask(id, agentId, store) {
|
|
|
2101
2347
|
locked_at: new Date().toISOString(),
|
|
2102
2348
|
started_at: task.started_at ?? new Date().toISOString()
|
|
2103
2349
|
}, store);
|
|
2350
|
+
await logTaskChange(task.id, "start", "status", task.status, "in_progress", agentId, store);
|
|
2351
|
+
return started;
|
|
2104
2352
|
}
|
|
2105
2353
|
async function completeTask(id, agentId, options, store) {
|
|
2106
2354
|
const task = await store.completeTask(id, agentId, options);
|
|
@@ -2767,6 +3015,7 @@ var CLOUD_LOCK_EXPIRY_MINUTES = 30, TASK_ORDER_TIEBREAK = "CASE payload->>'prior
|
|
|
2767
3015
|
var init_postgres_adapter = __esm(() => {
|
|
2768
3016
|
init_types();
|
|
2769
3017
|
init_postgres_sync();
|
|
3018
|
+
init_integrity();
|
|
2770
3019
|
init_redaction();
|
|
2771
3020
|
TASK_ORDER_BY = `ORDER BY ${TASK_ORDER_TIEBREAK}`;
|
|
2772
3021
|
});
|
|
@@ -4765,6 +5014,7 @@ __export(exports_cloud, {
|
|
|
4765
5014
|
resolveCloudDatabaseUrl: () => resolveCloudDatabaseUrl,
|
|
4766
5015
|
pingCloud: () => pingCloud,
|
|
4767
5016
|
normalizeCloudPayloads: () => normalizeCloudPayloads,
|
|
5017
|
+
isPostgresBackendConfigured: () => isPostgresBackendConfigured,
|
|
4768
5018
|
isCloudModeEnabled: () => isCloudModeEnabled,
|
|
4769
5019
|
getCloudVerifier: () => getCloudVerifier,
|
|
4770
5020
|
getCloudStorageAdapter: () => getCloudStorageAdapter,
|
|
@@ -4787,9 +5037,12 @@ function resolveCloudDatabaseUrl(env = process.env) {
|
|
|
4787
5037
|
function resolveSigningSecret(env = process.env) {
|
|
4788
5038
|
return env.HASNA_TODOS_API_SIGNING_KEY || env.HASNA_API_SIGNING_KEY || env.API_KEY_SIGNING_SECRET || undefined;
|
|
4789
5039
|
}
|
|
4790
|
-
function
|
|
5040
|
+
function isPostgresBackendConfigured(env = process.env) {
|
|
4791
5041
|
return Boolean(resolveCloudDatabaseUrl(env));
|
|
4792
5042
|
}
|
|
5043
|
+
function isCloudModeEnabled(env = process.env) {
|
|
5044
|
+
return isPostgresBackendConfigured(env);
|
|
5045
|
+
}
|
|
4793
5046
|
function getClient() {
|
|
4794
5047
|
if (cachedClient)
|
|
4795
5048
|
return cachedClient;
|
|
@@ -8617,6 +8870,30 @@ function findGitRoot(startDir) {
|
|
|
8617
8870
|
}
|
|
8618
8871
|
return null;
|
|
8619
8872
|
}
|
|
8873
|
+
function getGlobalDbPath() {
|
|
8874
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
8875
|
+
return join3(home, ".hasna", "todos", "todos.db");
|
|
8876
|
+
}
|
|
8877
|
+
function hasExplicitProjectArg(args = process.argv.slice(2)) {
|
|
8878
|
+
return args.some((arg) => arg === "--project" || arg.startsWith("--project="));
|
|
8879
|
+
}
|
|
8880
|
+
function getCliCommand(args = process.argv.slice(2)) {
|
|
8881
|
+
const globalOptionsWithValues = new Set(["--project", "--agent", "--session"]);
|
|
8882
|
+
for (let index = 0;index < args.length; index += 1) {
|
|
8883
|
+
const arg = args[index];
|
|
8884
|
+
if (globalOptionsWithValues.has(arg)) {
|
|
8885
|
+
index += 1;
|
|
8886
|
+
continue;
|
|
8887
|
+
}
|
|
8888
|
+
if (arg.startsWith("-"))
|
|
8889
|
+
continue;
|
|
8890
|
+
return arg;
|
|
8891
|
+
}
|
|
8892
|
+
return null;
|
|
8893
|
+
}
|
|
8894
|
+
function canCreateScopedProjectDb(args = process.argv.slice(2)) {
|
|
8895
|
+
return getCliCommand(args) === "project-bootstrap" && !args.some((arg) => arg === "--dry-run" || arg.startsWith("--dry-run="));
|
|
8896
|
+
}
|
|
8620
8897
|
function getDbPath() {
|
|
8621
8898
|
if (process.env["HASNA_TODOS_DB_PATH"]) {
|
|
8622
8899
|
return process.env["HASNA_TODOS_DB_PATH"];
|
|
@@ -8624,18 +8901,19 @@ function getDbPath() {
|
|
|
8624
8901
|
if (process.env["TODOS_DB_PATH"]) {
|
|
8625
8902
|
return process.env["TODOS_DB_PATH"];
|
|
8626
8903
|
}
|
|
8904
|
+
if (hasExplicitProjectArg())
|
|
8905
|
+
return getGlobalDbPath();
|
|
8627
8906
|
const cwd = process.cwd();
|
|
8628
8907
|
const nearest = findNearestProjectDb(cwd);
|
|
8629
8908
|
if (nearest)
|
|
8630
8909
|
return nearest;
|
|
8631
8910
|
if (process.env["TODOS_DB_SCOPE"] === "project") {
|
|
8632
8911
|
const gitRoot = findGitRoot(cwd);
|
|
8633
|
-
if (gitRoot) {
|
|
8912
|
+
if (gitRoot && canCreateScopedProjectDb()) {
|
|
8634
8913
|
return join3(gitRoot, ".hasna", "todos", "todos.db");
|
|
8635
8914
|
}
|
|
8636
8915
|
}
|
|
8637
|
-
|
|
8638
|
-
return join3(home, ".hasna", "todos", "todos.db");
|
|
8916
|
+
return getGlobalDbPath();
|
|
8639
8917
|
}
|
|
8640
8918
|
function getDatabasePath() {
|
|
8641
8919
|
return getDbPath();
|
|
@@ -8738,10 +9016,13 @@ function resolvePartialId(db, table, partialId) {
|
|
|
8738
9016
|
return null;
|
|
8739
9017
|
}
|
|
8740
9018
|
if (table === "tasks") {
|
|
8741
|
-
const shortIdRows = db.query("SELECT id FROM tasks WHERE short_id = ?").all(partialId);
|
|
9019
|
+
const shortIdRows = db.query("SELECT id, project_id FROM tasks WHERE LOWER(short_id) = LOWER(?) ORDER BY project_id, id").all(partialId);
|
|
8742
9020
|
if (shortIdRows.length === 1) {
|
|
8743
9021
|
return shortIdRows[0].id;
|
|
8744
9022
|
}
|
|
9023
|
+
if (shortIdRows.length > 1) {
|
|
9024
|
+
throw new TaskReferenceAmbiguousError(partialId, shortIdRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
|
|
9025
|
+
}
|
|
8745
9026
|
}
|
|
8746
9027
|
if (table === "task_lists") {
|
|
8747
9028
|
const slugRow = db.query("SELECT id FROM task_lists WHERE slug = ?").get(partialId);
|
|
@@ -8905,7 +9186,7 @@ function authNotConfiguredMessage(host) {
|
|
|
8905
9186
|
`);
|
|
8906
9187
|
}
|
|
8907
9188
|
function resolveAuthPosture(input) {
|
|
8908
|
-
const hosted = input.hosted ??
|
|
9189
|
+
const hosted = input.hosted ?? isPostgresBackendConfigured();
|
|
8909
9190
|
const hasCredentialSource = Boolean(input.apiKey) || input.hasGeneratedKeys;
|
|
8910
9191
|
if (hasCredentialSource) {
|
|
8911
9192
|
return {
|
|
@@ -17227,6 +17508,32 @@ var init_headless_boundaries = __esm(() => {
|
|
|
17227
17508
|
LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
17228
17509
|
});
|
|
17229
17510
|
|
|
17511
|
+
// src/db/integrity.ts
|
|
17512
|
+
function tableExists(db, table) {
|
|
17513
|
+
return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
|
|
17514
|
+
}
|
|
17515
|
+
function scanSqliteIntegrity(db = getDatabase()) {
|
|
17516
|
+
const missing = Object.keys(REQUIRED_TABLES).filter((table) => !tableExists(db, table));
|
|
17517
|
+
const conditions = INTEGRITY_CONDITIONS.map((spec) => {
|
|
17518
|
+
if (missing.length > 0) {
|
|
17519
|
+
return unverifiedCondition(spec, `local schema is missing table(s): ${missing.join(", ")}`);
|
|
17520
|
+
}
|
|
17521
|
+
try {
|
|
17522
|
+
const row = db.query(buildSqliteIntegritySql(spec)).get();
|
|
17523
|
+
return measuredCondition(spec, { count: Number(row?.count ?? 0), open_count: spec.entity === "task" ? Number(row?.open_count ?? 0) : null }, "sqlite");
|
|
17524
|
+
} catch (error) {
|
|
17525
|
+
return unverifiedCondition(spec, error instanceof Error ? error.message : String(error));
|
|
17526
|
+
}
|
|
17527
|
+
});
|
|
17528
|
+
return buildIntegrityReport(conditions, now());
|
|
17529
|
+
}
|
|
17530
|
+
var REQUIRED_TABLES;
|
|
17531
|
+
var init_integrity2 = __esm(() => {
|
|
17532
|
+
init_database();
|
|
17533
|
+
init_integrity();
|
|
17534
|
+
REQUIRED_TABLES = { tasks: true, task_lists: true, projects: true };
|
|
17535
|
+
});
|
|
17536
|
+
|
|
17230
17537
|
// src/lib/doctor.ts
|
|
17231
17538
|
var exports_doctor = {};
|
|
17232
17539
|
__export(exports_doctor, {
|
|
@@ -17234,7 +17541,7 @@ __export(exports_doctor, {
|
|
|
17234
17541
|
});
|
|
17235
17542
|
import { chmodSync, copyFileSync, existsSync as existsSync7, mkdirSync as mkdirSync5, statSync as statSync3 } from "fs";
|
|
17236
17543
|
import { basename as basename2, dirname as dirname5, join as join6 } from "path";
|
|
17237
|
-
function
|
|
17544
|
+
function tableExists2(db, table) {
|
|
17238
17545
|
return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
|
|
17239
17546
|
}
|
|
17240
17547
|
function quoteIdent(identifier) {
|
|
@@ -17318,7 +17625,7 @@ function findDuplicateIndexes(db) {
|
|
|
17318
17625
|
return duplicates;
|
|
17319
17626
|
}
|
|
17320
17627
|
function findMissingProjectRoots(db) {
|
|
17321
|
-
if (!
|
|
17628
|
+
if (!tableExists2(db, "projects"))
|
|
17322
17629
|
return 0;
|
|
17323
17630
|
let missing = 0;
|
|
17324
17631
|
const rows = db.query("SELECT path FROM projects WHERE path IS NOT NULL AND path != ''").all();
|
|
@@ -17333,7 +17640,7 @@ function findMissingProjectRoots(db) {
|
|
|
17333
17640
|
return missing;
|
|
17334
17641
|
}
|
|
17335
17642
|
function addTaskStateChecks(db, checks) {
|
|
17336
|
-
if (!
|
|
17643
|
+
if (!tableExists2(db, "tasks"))
|
|
17337
17644
|
return;
|
|
17338
17645
|
const columns = getTableColumns(db, "tasks");
|
|
17339
17646
|
const staleCutoff = new Date(Date.now() - 30 * 60 * 1000).toISOString();
|
|
@@ -17405,15 +17712,42 @@ function createBackup(dbPath) {
|
|
|
17405
17712
|
function pushRepair(repairs, type, message, applied, count) {
|
|
17406
17713
|
repairs.push({ type, message, applied, count });
|
|
17407
17714
|
}
|
|
17408
|
-
function summarize2(checks, repairs) {
|
|
17715
|
+
function summarize2(checks, repairs, integrity) {
|
|
17409
17716
|
return {
|
|
17410
17717
|
errors: checks.filter((check) => check.severity === "error").length,
|
|
17411
17718
|
warnings: checks.filter((check) => check.severity === "warn").length,
|
|
17412
17719
|
infos: checks.filter((check) => check.severity === "info").length,
|
|
17413
17720
|
repairable: checks.filter((check) => check.repairable).length,
|
|
17414
|
-
applied: repairs.filter((repair) => repair.applied).length
|
|
17721
|
+
applied: repairs.filter((repair) => repair.applied).length,
|
|
17722
|
+
integrity_findings: integrity.summary.findings,
|
|
17723
|
+
integrity_rows: integrity.summary.rows,
|
|
17724
|
+
integrity_unverified: integrity.summary.unverified
|
|
17415
17725
|
};
|
|
17416
17726
|
}
|
|
17727
|
+
function addIntegrityChecks(integrity, checks) {
|
|
17728
|
+
for (const condition of integrity.conditions) {
|
|
17729
|
+
if (!condition.verified) {
|
|
17730
|
+
addCheck(checks, {
|
|
17731
|
+
severity: "warn",
|
|
17732
|
+
type: `${condition.id}_unverified`,
|
|
17733
|
+
message: condition.message,
|
|
17734
|
+
repairable: false,
|
|
17735
|
+
integrity: true
|
|
17736
|
+
});
|
|
17737
|
+
continue;
|
|
17738
|
+
}
|
|
17739
|
+
if ((condition.count ?? 0) === 0)
|
|
17740
|
+
continue;
|
|
17741
|
+
addCheck(checks, {
|
|
17742
|
+
severity: condition.severity ?? "warn",
|
|
17743
|
+
type: condition.id,
|
|
17744
|
+
message: `${condition.message} \u2014 ${condition.impact}`,
|
|
17745
|
+
count: condition.count ?? undefined,
|
|
17746
|
+
repairable: false,
|
|
17747
|
+
integrity: true
|
|
17748
|
+
});
|
|
17749
|
+
}
|
|
17750
|
+
}
|
|
17417
17751
|
function deleteOrphans(db, table, where) {
|
|
17418
17752
|
const before = countQuery(db, `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`);
|
|
17419
17753
|
if (before > 0)
|
|
@@ -17442,7 +17776,7 @@ function runTodosDoctor(options = {}) {
|
|
|
17442
17776
|
message: `Schema at migration ${migrationCurrent}`
|
|
17443
17777
|
});
|
|
17444
17778
|
}
|
|
17445
|
-
const missingTables =
|
|
17779
|
+
const missingTables = REQUIRED_TABLES2.filter((table) => !tableExists2(db, table));
|
|
17446
17780
|
if (missingTables.length > 0) {
|
|
17447
17781
|
addCheck(checks, {
|
|
17448
17782
|
severity: "error",
|
|
@@ -17452,7 +17786,7 @@ function runTodosDoctor(options = {}) {
|
|
|
17452
17786
|
repairable: true
|
|
17453
17787
|
});
|
|
17454
17788
|
}
|
|
17455
|
-
const orphanedParents =
|
|
17789
|
+
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;
|
|
17456
17790
|
if (orphanedParents > 0) {
|
|
17457
17791
|
addCheck(checks, {
|
|
17458
17792
|
severity: "error",
|
|
@@ -17462,7 +17796,7 @@ function runTodosDoctor(options = {}) {
|
|
|
17462
17796
|
repairable: true
|
|
17463
17797
|
});
|
|
17464
17798
|
}
|
|
17465
|
-
const orphanedDependencies =
|
|
17799
|
+
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;
|
|
17466
17800
|
if (orphanedDependencies > 0) {
|
|
17467
17801
|
addCheck(checks, {
|
|
17468
17802
|
severity: "error",
|
|
@@ -17481,7 +17815,7 @@ function runTodosDoctor(options = {}) {
|
|
|
17481
17815
|
];
|
|
17482
17816
|
let orphanedRows = 0;
|
|
17483
17817
|
for (const [table, where] of orphanTables) {
|
|
17484
|
-
if (!
|
|
17818
|
+
if (!tableExists2(db, table))
|
|
17485
17819
|
continue;
|
|
17486
17820
|
orphanedRows += countQuery(db, `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`);
|
|
17487
17821
|
}
|
|
@@ -17495,6 +17829,8 @@ function runTodosDoctor(options = {}) {
|
|
|
17495
17829
|
});
|
|
17496
17830
|
}
|
|
17497
17831
|
addTaskStateChecks(db, checks);
|
|
17832
|
+
const integrity = scanSqliteIntegrity(db);
|
|
17833
|
+
addIntegrityChecks(integrity, checks);
|
|
17498
17834
|
const corruptJson = findCorruptJsonMetadata(db);
|
|
17499
17835
|
if (corruptJson.length > 0) {
|
|
17500
17836
|
addCheck(checks, {
|
|
@@ -17553,12 +17889,12 @@ function runTodosDoctor(options = {}) {
|
|
|
17553
17889
|
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)");
|
|
17554
17890
|
pushRepair(repairs, "orphaned_task_parents", "Cleared missing parent references", true, orphanedParents);
|
|
17555
17891
|
}
|
|
17556
|
-
if (orphanedDependencies > 0 &&
|
|
17892
|
+
if (orphanedDependencies > 0 && tableExists2(db, "task_dependencies")) {
|
|
17557
17893
|
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)");
|
|
17558
17894
|
pushRepair(repairs, "orphaned_task_dependencies", "Deleted dependency rows referencing missing tasks", true, count);
|
|
17559
17895
|
}
|
|
17560
17896
|
for (const [table, where] of orphanTables) {
|
|
17561
|
-
if (!
|
|
17897
|
+
if (!tableExists2(db, table))
|
|
17562
17898
|
continue;
|
|
17563
17899
|
const count = deleteOrphans(db, table, where);
|
|
17564
17900
|
if (count > 0)
|
|
@@ -17587,26 +17923,30 @@ function runTodosDoctor(options = {}) {
|
|
|
17587
17923
|
}
|
|
17588
17924
|
}
|
|
17589
17925
|
}
|
|
17590
|
-
const
|
|
17591
|
-
const
|
|
17926
|
+
const reread = apply && hasRepairableIssue ? runTodosDoctor({ db, dbPath, apply: false }) : undefined;
|
|
17927
|
+
const finalChecks = reread?.checks ?? checks;
|
|
17928
|
+
const finalIntegrity = reread?.integrity ?? integrity;
|
|
17929
|
+
const summary = summarize2(finalChecks, repairs, finalIntegrity);
|
|
17592
17930
|
return {
|
|
17593
|
-
ok: !finalChecks.some((check) => check.severity === "error"),
|
|
17931
|
+
ok: !finalChecks.some((check) => check.severity === "error") && finalIntegrity.summary.ok,
|
|
17594
17932
|
dry_run: !apply,
|
|
17595
17933
|
database_path: dbPath,
|
|
17596
17934
|
migration: { current: getMigrationLevel(db), expected: migrationExpected },
|
|
17597
17935
|
backup,
|
|
17598
17936
|
checks: finalChecks,
|
|
17599
17937
|
repairs,
|
|
17938
|
+
integrity: finalIntegrity,
|
|
17600
17939
|
summary
|
|
17601
17940
|
};
|
|
17602
17941
|
}
|
|
17603
|
-
var
|
|
17942
|
+
var REQUIRED_TABLES2;
|
|
17604
17943
|
var init_doctor = __esm(() => {
|
|
17605
17944
|
init_database();
|
|
17945
|
+
init_integrity2();
|
|
17606
17946
|
init_migrations();
|
|
17607
17947
|
init_schema();
|
|
17608
17948
|
init_recurrence();
|
|
17609
|
-
|
|
17949
|
+
REQUIRED_TABLES2 = [
|
|
17610
17950
|
"_migrations",
|
|
17611
17951
|
"projects",
|
|
17612
17952
|
"tasks",
|
|
@@ -18998,15 +19338,15 @@ function resolveTaskRefLocal(db, ref) {
|
|
|
18998
19338
|
return null;
|
|
18999
19339
|
if (TASK_UUID_RE.test(raw))
|
|
19000
19340
|
return getTask(raw, db);
|
|
19001
|
-
const prefixRows = db.query("SELECT id FROM tasks WHERE LOWER(id) LIKE ? ESCAPE '\\' LIMIT 2").all(`${raw.replace(/[\\%_]/g, (c) => `\\${c}`)}%`);
|
|
19341
|
+
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}`)}%`);
|
|
19002
19342
|
if (prefixRows.length > 1) {
|
|
19003
|
-
throw new
|
|
19343
|
+
throw new TaskReferenceAmbiguousError(ref, prefixRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
|
|
19004
19344
|
}
|
|
19005
19345
|
if (prefixRows.length === 1)
|
|
19006
19346
|
return getTask(prefixRows[0].id, db);
|
|
19007
|
-
const shortIdRows = db.query("SELECT id FROM tasks WHERE LOWER(short_id) = ? LIMIT 2").all(raw);
|
|
19347
|
+
const shortIdRows = db.query("SELECT id, project_id FROM tasks WHERE LOWER(short_id) = ? ORDER BY project_id, id LIMIT 2").all(raw);
|
|
19008
19348
|
if (shortIdRows.length > 1) {
|
|
19009
|
-
throw new
|
|
19349
|
+
throw new TaskReferenceAmbiguousError(ref, shortIdRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
|
|
19010
19350
|
}
|
|
19011
19351
|
if (shortIdRows.length === 1)
|
|
19012
19352
|
return getTask(shortIdRows[0].id, db);
|
|
@@ -19155,6 +19495,9 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
19155
19495
|
exportSnapshot: () => exportSqliteTodosStorageSnapshot(database()),
|
|
19156
19496
|
importSnapshot: (snapshot) => importSqliteTodosStorageSnapshot(snapshot, database())
|
|
19157
19497
|
},
|
|
19498
|
+
integrity: {
|
|
19499
|
+
report: () => scanSqliteIntegrity(database())
|
|
19500
|
+
},
|
|
19158
19501
|
transaction: (fn) => {
|
|
19159
19502
|
const tx = database().transaction(() => fn(adapter));
|
|
19160
19503
|
return tx();
|
|
@@ -19164,6 +19507,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
19164
19507
|
}
|
|
19165
19508
|
var TASK_UUID_RE;
|
|
19166
19509
|
var init_local_sqlite = __esm(() => {
|
|
19510
|
+
init_types();
|
|
19167
19511
|
init_search();
|
|
19168
19512
|
init_tasks();
|
|
19169
19513
|
init_projects();
|
|
@@ -19174,6 +19518,7 @@ var init_local_sqlite = __esm(() => {
|
|
|
19174
19518
|
init_audit();
|
|
19175
19519
|
init_comments();
|
|
19176
19520
|
init_database();
|
|
19521
|
+
init_integrity2();
|
|
19177
19522
|
init_sqlite_snapshot();
|
|
19178
19523
|
TASK_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
19179
19524
|
});
|
|
@@ -20472,6 +20817,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
20472
20817
|
{ name: "task_list_id", in: "query", schema: { type: "string" } },
|
|
20473
20818
|
{ name: "assigned_to", in: "query", schema: { type: "string" } },
|
|
20474
20819
|
{ name: "agent_id", in: "query", schema: { type: "string" } },
|
|
20820
|
+
{ name: "tags", in: "query", schema: { type: "string" }, description: "Comma-separated tags; matches tasks carrying any of them" },
|
|
20475
20821
|
{ name: "limit", in: "query", schema: { type: "integer", minimum: 1 } },
|
|
20476
20822
|
{ name: "offset", in: "query", schema: { type: "integer", minimum: 0 } }
|
|
20477
20823
|
],
|
|
@@ -21676,6 +22022,9 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
21676
22022
|
...url.searchParams.get("task_list_id") ? { task_list_id: url.searchParams.get("task_list_id") } : {},
|
|
21677
22023
|
...url.searchParams.get("assigned_to") ? { assigned_to: url.searchParams.get("assigned_to") } : {},
|
|
21678
22024
|
...url.searchParams.get("agent_id") ? { agent_id: url.searchParams.get("agent_id") } : {},
|
|
22025
|
+
...url.searchParams.get("tags") ? {
|
|
22026
|
+
tags: url.searchParams.get("tags").split(",").map((tag) => tag.trim()).filter(Boolean)
|
|
22027
|
+
} : {},
|
|
21679
22028
|
...url.searchParams.get("limit") ? { limit: Number(url.searchParams.get("limit")) } : {},
|
|
21680
22029
|
...url.searchParams.get("offset") ? { offset: Number(url.searchParams.get("offset")) } : {}
|
|
21681
22030
|
};
|
|
@@ -21957,6 +22306,13 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
21957
22306
|
try {
|
|
21958
22307
|
task = await store.tasks.resolveRef(id);
|
|
21959
22308
|
} catch (e) {
|
|
22309
|
+
if (e instanceof TaskReferenceAmbiguousError) {
|
|
22310
|
+
return error(409, e.message, {
|
|
22311
|
+
code: TaskReferenceAmbiguousError.code,
|
|
22312
|
+
candidate_project_ids: e.candidateProjectIds,
|
|
22313
|
+
candidate_task_ids: e.candidateTaskIds
|
|
22314
|
+
});
|
|
22315
|
+
}
|
|
21960
22316
|
const msg = e.message || "";
|
|
21961
22317
|
if (/ambiguous/i.test(msg))
|
|
21962
22318
|
return error(409, msg);
|
|
@@ -22314,6 +22670,15 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
22314
22670
|
]);
|
|
22315
22671
|
return json3({ tasks, tasks_all: tasksAll, subtasks: tasksAll - tasks, projects: projects.length });
|
|
22316
22672
|
}
|
|
22673
|
+
if (resource === "integrity" && !id) {
|
|
22674
|
+
if (method !== "GET")
|
|
22675
|
+
return error(405, `method ${method} not allowed on /v1/integrity`);
|
|
22676
|
+
if (typeof store.integrity?.report !== "function") {
|
|
22677
|
+
return error(501, "referential-integrity reporting is not supported by this storage backend");
|
|
22678
|
+
}
|
|
22679
|
+
const integrity = await store.integrity.report();
|
|
22680
|
+
return json3({ integrity });
|
|
22681
|
+
}
|
|
22317
22682
|
if (resource === "import") {
|
|
22318
22683
|
if (method !== "POST")
|
|
22319
22684
|
return error(405, `method ${method} not allowed on /v1/import`);
|
|
@@ -22333,6 +22698,13 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
22333
22698
|
}
|
|
22334
22699
|
return error(404, `unknown /v1 resource: ${resource ?? "(root)"}`);
|
|
22335
22700
|
} catch (e) {
|
|
22701
|
+
if (e instanceof TaskReferenceAmbiguousError) {
|
|
22702
|
+
return error(409, e.message, {
|
|
22703
|
+
code: TaskReferenceAmbiguousError.code,
|
|
22704
|
+
candidate_project_ids: e.candidateProjectIds,
|
|
22705
|
+
candidate_task_ids: e.candidateTaskIds
|
|
22706
|
+
});
|
|
22707
|
+
}
|
|
22336
22708
|
if (e instanceof LockError)
|
|
22337
22709
|
return error(409, e.message, { code: LockError.code });
|
|
22338
22710
|
if (e instanceof ResourceConflictError)
|
|
@@ -44665,9 +45037,6 @@ function cleanMode(value) {
|
|
|
44665
45037
|
const normalized = value?.trim().toLowerCase();
|
|
44666
45038
|
return normalized || null;
|
|
44667
45039
|
}
|
|
44668
|
-
function modeRole(mode) {
|
|
44669
|
-
return mode === "local" ? "local" : "remote";
|
|
44670
|
-
}
|
|
44671
45040
|
function resolveTodosCliStorageMode(env = process.env) {
|
|
44672
45041
|
for (const source of ["HASNA_TODOS_STORAGE_MODE", "TODOS_STORAGE_MODE"]) {
|
|
44673
45042
|
if (env[source] !== undefined && env[source].trim() === "") {
|
|
@@ -44680,22 +45049,25 @@ function resolveTodosCliStorageMode(env = process.env) {
|
|
|
44680
45049
|
["HASNA_TODOS_STORAGE_MODE", canonical],
|
|
44681
45050
|
["TODOS_STORAGE_MODE", fallback]
|
|
44682
45051
|
]) {
|
|
44683
|
-
if (value && !
|
|
44684
|
-
throw new Error(`REMOTE_STORAGE_MODE_INVALID: ${source}=${value} must be local
|
|
45052
|
+
if (value && !(value in TRANSPORT_TOKENS)) {
|
|
45053
|
+
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");
|
|
44685
45054
|
}
|
|
44686
45055
|
}
|
|
44687
|
-
|
|
45056
|
+
const canonicalTransport = canonical ? TRANSPORT_TOKENS[canonical] : null;
|
|
45057
|
+
const fallbackTransport = fallback ? TRANSPORT_TOKENS[fallback] : null;
|
|
45058
|
+
if (canonicalTransport && fallbackTransport && canonicalTransport !== fallbackTransport) {
|
|
44688
45059
|
throw new Error(`REMOTE_STORAGE_MODE_CONFLICT: HASNA_TODOS_STORAGE_MODE=${canonical} conflicts with ` + `TODOS_STORAGE_MODE=${fallback}; local SQLite fallback is disabled`);
|
|
44689
45060
|
}
|
|
44690
|
-
const
|
|
45061
|
+
const transport = canonicalTransport ?? fallbackTransport ?? "sqlite";
|
|
44691
45062
|
return {
|
|
44692
|
-
mode,
|
|
44693
|
-
|
|
45063
|
+
mode: transport,
|
|
45064
|
+
transport,
|
|
45065
|
+
selected: transport === "http",
|
|
44694
45066
|
source: canonical ? "HASNA_TODOS_STORAGE_MODE" : fallback ? "TODOS_STORAGE_MODE" : "default"
|
|
44695
45067
|
};
|
|
44696
45068
|
}
|
|
44697
|
-
function
|
|
44698
|
-
return resolveTodosCliStorageMode(env).
|
|
45069
|
+
function requestedTransport(env) {
|
|
45070
|
+
return resolveTodosCliStorageMode(env).transport;
|
|
44699
45071
|
}
|
|
44700
45072
|
function normalizeRemoteAuthorityUrl(value) {
|
|
44701
45073
|
const trimmed = value?.trim();
|
|
@@ -44748,7 +45120,7 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env) {
|
|
|
44748
45120
|
return {
|
|
44749
45121
|
selected: false,
|
|
44750
45122
|
ok: true,
|
|
44751
|
-
mode
|
|
45123
|
+
mode,
|
|
44752
45124
|
api_url_configured: false,
|
|
44753
45125
|
api_key_configured: false,
|
|
44754
45126
|
v1_base_url: null,
|
|
@@ -44860,8 +45232,7 @@ async function requiredRemoteRoute(client, route, request) {
|
|
|
44860
45232
|
}
|
|
44861
45233
|
}
|
|
44862
45234
|
function getTodosCloudClient(env = process.env) {
|
|
44863
|
-
|
|
44864
|
-
if (!CLOUD_MODES.has(mode))
|
|
45235
|
+
if (requestedTransport(env) !== "http")
|
|
44865
45236
|
return null;
|
|
44866
45237
|
const resolved = resolveStorageClient("todos", requireTodosRemoteAuthorityEnv(env), {
|
|
44867
45238
|
fetchImpl: (input, init) => globalThis.fetch(input, { ...init, redirect: "manual" })
|
|
@@ -44896,13 +45267,38 @@ function toListQuery(filter = {}) {
|
|
|
44896
45267
|
query["assigned_to"] = filter.assigned_to;
|
|
44897
45268
|
if (filter.agent_id)
|
|
44898
45269
|
query["agent_id"] = filter.agent_id;
|
|
45270
|
+
if (filter.tags?.length)
|
|
45271
|
+
query["tags"] = filter.tags.join(",");
|
|
44899
45272
|
if (typeof filter.limit === "number")
|
|
44900
45273
|
query["limit"] = filter.limit;
|
|
44901
45274
|
if (typeof filter.offset === "number")
|
|
44902
45275
|
query["offset"] = filter.offset;
|
|
44903
45276
|
return query;
|
|
44904
45277
|
}
|
|
45278
|
+
async function fetchListTagsCapability(client) {
|
|
45279
|
+
const document = await requiredRemoteRoute(client, "/v1/openapi.json", () => client.transport.get("/openapi.json"));
|
|
45280
|
+
if (!document || typeof document !== "object" || Array.isArray(document))
|
|
45281
|
+
return false;
|
|
45282
|
+
const paths = document["paths"];
|
|
45283
|
+
const tasksPath = paths && typeof paths === "object" && !Array.isArray(paths) ? paths["/v1/tasks"] : undefined;
|
|
45284
|
+
const get = tasksPath && typeof tasksPath === "object" && !Array.isArray(tasksPath) ? tasksPath["get"] : undefined;
|
|
45285
|
+
const parameters = get && typeof get === "object" && !Array.isArray(get) ? get["parameters"] : undefined;
|
|
45286
|
+
return Array.isArray(parameters) && parameters.some((parameter) => parameter && typeof parameter === "object" && parameter["name"] === "tags");
|
|
45287
|
+
}
|
|
45288
|
+
async function requireTagsFilterCapability(client) {
|
|
45289
|
+
const authority = remoteAuthorityBase(client);
|
|
45290
|
+
let capability = listTagsCapabilityCache.get(authority);
|
|
45291
|
+
if (!capability) {
|
|
45292
|
+
capability = fetchListTagsCapability(client);
|
|
45293
|
+
listTagsCapabilityCache.set(authority, capability);
|
|
45294
|
+
}
|
|
45295
|
+
if (!await capability) {
|
|
45296
|
+
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");
|
|
45297
|
+
}
|
|
45298
|
+
}
|
|
44905
45299
|
async function cloudListTasks(client, filter = {}) {
|
|
45300
|
+
if (filter.tags?.length)
|
|
45301
|
+
await requireTagsFilterCapability(client);
|
|
44906
45302
|
const res = await requiredRemoteRoute(client, "/v1/tasks", () => client.list("tasks", { query: toListQuery(filter) }));
|
|
44907
45303
|
const envelope = res.raw;
|
|
44908
45304
|
return Array.isArray(envelope?.tasks) ? envelope.tasks : res.items;
|
|
@@ -45010,6 +45406,8 @@ function redactComment3(comment) {
|
|
|
45010
45406
|
}
|
|
45011
45407
|
async function cloudCountTasks(client, filter = {}) {
|
|
45012
45408
|
const { limit: _drop, offset: _o, ...rest } = filter;
|
|
45409
|
+
if (rest.tags?.length)
|
|
45410
|
+
await requireTagsFilterCapability(client);
|
|
45013
45411
|
const res = await requiredRemoteRoute(client, "/v1/tasks", () => client.list("tasks", { query: { ...toListQuery(rest), limit: 1 } }));
|
|
45014
45412
|
const envelope = res.raw;
|
|
45015
45413
|
if (typeof envelope?.total === "number")
|
|
@@ -45072,15 +45470,23 @@ async function cloudResolveTaskListRef(client, ref, projectId) {
|
|
|
45072
45470
|
}
|
|
45073
45471
|
throw new Error(`Task list not found: "${input}"`);
|
|
45074
45472
|
}
|
|
45075
|
-
var UUID_RE,
|
|
45473
|
+
var UUID_RE, TRANSPORT_TOKENS, completionCapabilityCache, listTagsCapabilityCache;
|
|
45076
45474
|
var init_cloud_router = __esm(() => {
|
|
45077
45475
|
init_types();
|
|
45078
45476
|
init_redaction();
|
|
45079
45477
|
init_http_client();
|
|
45080
45478
|
UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
45081
|
-
|
|
45082
|
-
|
|
45479
|
+
TRANSPORT_TOKENS = {
|
|
45480
|
+
sqlite: "sqlite",
|
|
45481
|
+
http: "http",
|
|
45482
|
+
local: "sqlite",
|
|
45483
|
+
remote: "http",
|
|
45484
|
+
self_hosted: "http",
|
|
45485
|
+
cloud: "http",
|
|
45486
|
+
hybrid: "http"
|
|
45487
|
+
};
|
|
45083
45488
|
completionCapabilityCache = new Map;
|
|
45489
|
+
listTagsCapabilityCache = new Map;
|
|
45084
45490
|
});
|
|
45085
45491
|
|
|
45086
45492
|
// src/mcp/tools/task-crud.ts
|
|
@@ -48666,12 +49072,11 @@ ${task.description || ""}`.split(/\r?\n/);
|
|
|
48666
49072
|
function normalizeStackLocation(value) {
|
|
48667
49073
|
return value.replace(/:\d+(?::\d+)?/g, "").replace(/^file:\/\//, "").toLowerCase().trim();
|
|
48668
49074
|
}
|
|
48669
|
-
function
|
|
49075
|
+
function fingerprintContent(task) {
|
|
48670
49076
|
const body = task.description || "";
|
|
48671
49077
|
const text = `${task.title}
|
|
48672
49078
|
${body}`;
|
|
48673
49079
|
return {
|
|
48674
|
-
task,
|
|
48675
49080
|
title: normalizeText(task.title),
|
|
48676
49081
|
body: normalizeText(body),
|
|
48677
49082
|
text: normalizeText(text),
|
|
@@ -48680,6 +49085,9 @@ ${body}`;
|
|
|
48680
49085
|
stackKeys: stackKeysFor(task)
|
|
48681
49086
|
};
|
|
48682
49087
|
}
|
|
49088
|
+
function fingerprint(task) {
|
|
49089
|
+
return { task, ...fingerprintContent(task) };
|
|
49090
|
+
}
|
|
48683
49091
|
function intersects(left, right) {
|
|
48684
49092
|
for (const value of left)
|
|
48685
49093
|
if (right.has(value))
|
|
@@ -49888,7 +50296,7 @@ function tableColumns(db, table) {
|
|
|
49888
50296
|
}
|
|
49889
50297
|
function assertMigratedSchema(db) {
|
|
49890
50298
|
const issues = [];
|
|
49891
|
-
for (const table of
|
|
50299
|
+
for (const table of REQUIRED_TABLES3) {
|
|
49892
50300
|
if (!hasTable(db, table))
|
|
49893
50301
|
issues.push(`missing table ${table}`);
|
|
49894
50302
|
}
|
|
@@ -49996,7 +50404,7 @@ function createReleaseCompatibilityReport(options = {}) {
|
|
|
49996
50404
|
migrations: {
|
|
49997
50405
|
current_level: MIGRATIONS.length,
|
|
49998
50406
|
simulated_levels: simulatedLevels,
|
|
49999
|
-
checked_tables:
|
|
50407
|
+
checked_tables: REQUIRED_TABLES3,
|
|
50000
50408
|
checked_columns: REQUIRED_COLUMNS
|
|
50001
50409
|
},
|
|
50002
50410
|
exports: {
|
|
@@ -50016,7 +50424,7 @@ function createReleaseCompatibilityReport(options = {}) {
|
|
|
50016
50424
|
"todos --help",
|
|
50017
50425
|
"todos-mcp --help",
|
|
50018
50426
|
"todos-serve --help",
|
|
50019
|
-
"todos doctor"
|
|
50427
|
+
"todos doctor --no-fail-on-findings"
|
|
50020
50428
|
],
|
|
50021
50429
|
smoke_tests: [
|
|
50022
50430
|
"command -v todos",
|
|
@@ -50024,7 +50432,7 @@ function createReleaseCompatibilityReport(options = {}) {
|
|
|
50024
50432
|
"command -v todos-serve",
|
|
50025
50433
|
"todos --version",
|
|
50026
50434
|
"todos --help",
|
|
50027
|
-
"todos doctor"
|
|
50435
|
+
"todos doctor --no-fail-on-findings"
|
|
50028
50436
|
],
|
|
50029
50437
|
rollback: [
|
|
50030
50438
|
"npm view @hasna/todos versions --json",
|
|
@@ -50073,14 +50481,14 @@ function renderReleaseCompatibilityMarkdown(report) {
|
|
|
50073
50481
|
return lines.join(`
|
|
50074
50482
|
`);
|
|
50075
50483
|
}
|
|
50076
|
-
var LOCAL_RELEASE_COMPATIBILITY_SCHEMA_VERSION = 1, EXPECTED_PACKAGE_NAME = "@hasna/todos", EXPECTED_REPOSITORY = "https://github.com/hasna/todos.git", EXPECTED_BINS, EXPECTED_EXPORTS, REQUIRED_SCRIPTS,
|
|
50484
|
+
var LOCAL_RELEASE_COMPATIBILITY_SCHEMA_VERSION = 1, EXPECTED_PACKAGE_NAME = "@hasna/todos", EXPECTED_REPOSITORY = "https://github.com/hasna/todos.git", EXPECTED_BINS, EXPECTED_EXPORTS, REQUIRED_SCRIPTS, REQUIRED_TABLES3, REQUIRED_COLUMNS;
|
|
50077
50485
|
var init_release_compatibility = __esm(() => {
|
|
50078
50486
|
init_migrations();
|
|
50079
50487
|
init_schema();
|
|
50080
50488
|
EXPECTED_BINS = ["todos", "todos-mcp", "todos-serve"];
|
|
50081
50489
|
EXPECTED_EXPORTS = [".", "./sdk", "./mcp", "./registry", "./contracts", "./storage"];
|
|
50082
50490
|
REQUIRED_SCRIPTS = ["build", "test:no-cloud", "verify:release", "prepublishOnly"];
|
|
50083
|
-
|
|
50491
|
+
REQUIRED_TABLES3 = [
|
|
50084
50492
|
"_migrations",
|
|
50085
50493
|
"projects",
|
|
50086
50494
|
"tasks",
|
|
@@ -53839,7 +54247,7 @@ function limits(input) {
|
|
|
53839
54247
|
verification_limit: clamp(input.verification_limit, DEFAULT_LIMITS.verification_limit, 100),
|
|
53840
54248
|
run_limit: clamp(input.run_limit, DEFAULT_LIMITS.run_limit, 20),
|
|
53841
54249
|
dependency_limit: clamp(input.dependency_limit, DEFAULT_LIMITS.dependency_limit, 100),
|
|
53842
|
-
plan_task_limit: clamp(input.plan_task_limit, DEFAULT_LIMITS.plan_task_limit,
|
|
54250
|
+
plan_task_limit: clamp(input.plan_task_limit, DEFAULT_LIMITS.plan_task_limit, MAX_CONTEXT_TASKS),
|
|
53843
54251
|
max_text_chars: clamp(input.max_text_chars, DEFAULT_LIMITS.max_text_chars, 50000),
|
|
53844
54252
|
summary_char_limit: clamp(input.summary_char_limit, DEFAULT_LIMITS.summary_char_limit, 4000),
|
|
53845
54253
|
stale_after_hours: clamp(input.stale_after_hours, DEFAULT_LIMITS.stale_after_hours, 24 * 365)
|
|
@@ -53862,6 +54270,13 @@ function taskSummary(task) {
|
|
|
53862
54270
|
priority: task.priority
|
|
53863
54271
|
};
|
|
53864
54272
|
}
|
|
54273
|
+
function latestPlanTasks(planId, limit, db) {
|
|
54274
|
+
const rows = db.query(`SELECT * FROM tasks
|
|
54275
|
+
WHERE plan_id = ? AND archived_at IS NULL
|
|
54276
|
+
ORDER BY updated_at DESC, created_at DESC, id ASC
|
|
54277
|
+
LIMIT ?`).all(planId, limit);
|
|
54278
|
+
return rows.map(rowToTask);
|
|
54279
|
+
}
|
|
53865
54280
|
function acceptanceCriteria(task, maxText) {
|
|
53866
54281
|
const metadata = task.metadata || {};
|
|
53867
54282
|
const raw = metadata["acceptance_criteria"] ?? metadata["acceptanceCriteria"] ?? metadata["criteria"];
|
|
@@ -53959,7 +54374,7 @@ function summarizeSection(pack, section, maxChars) {
|
|
|
53959
54374
|
return "No plan was attached.";
|
|
53960
54375
|
return summarizeStrings([
|
|
53961
54376
|
`Plan ${pack.plan.name} is ${pack.plan.status}`,
|
|
53962
|
-
`${pack.plan.tasks.length} listed plan
|
|
54377
|
+
`${pack.plan.tasks.length} of ${pack.plan.total_tasks} plan tasks listed; ${pack.plan.omitted_tasks} omitted`,
|
|
53963
54378
|
...pack.plan.tasks.slice(0, 4).map((task) => `${task.status} ${task.short_id || task.id.slice(0, 8)} ${task.title}`)
|
|
53964
54379
|
], maxChars);
|
|
53965
54380
|
}
|
|
@@ -54109,7 +54524,8 @@ function createAgentContextPack(input, db) {
|
|
|
54109
54524
|
const limit = limits(input);
|
|
54110
54525
|
const project = task.project_id ? getProject(task.project_id, d) : null;
|
|
54111
54526
|
const plan = task.plan_id ? getPlan(task.plan_id, d) : null;
|
|
54112
|
-
const planTasks = task.plan_id ?
|
|
54527
|
+
const planTasks = task.plan_id ? latestPlanTasks(task.plan_id, limit.plan_task_limit, d) : [];
|
|
54528
|
+
const totalPlanTasks = task.plan_id ? countTasks({ plan_id: task.plan_id }, d) : 0;
|
|
54113
54529
|
const upstream = getTaskDependencies(task.id, d).map((dep) => taskSummary(getTask(dep.depends_on, d))).filter((item) => Boolean(item));
|
|
54114
54530
|
const downstream = getTaskDependents(task.id, d).map((dep) => taskSummary(getTask(dep.task_id, d))).filter((item) => Boolean(item));
|
|
54115
54531
|
const comments = listComments(task.id, d);
|
|
@@ -54152,6 +54568,9 @@ function createAgentContextPack(input, db) {
|
|
|
54152
54568
|
warnings.push(`${runs.length - selectedRuns.length} older runs omitted`);
|
|
54153
54569
|
if (fileMap.size > relevantFiles.length)
|
|
54154
54570
|
warnings.push(`${fileMap.size - relevantFiles.length} relevant files omitted`);
|
|
54571
|
+
if (totalPlanTasks > planTasks.length && plan) {
|
|
54572
|
+
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>"`);
|
|
54573
|
+
}
|
|
54155
54574
|
const contextTask = {
|
|
54156
54575
|
id: task.id,
|
|
54157
54576
|
short_id: task.short_id,
|
|
@@ -54183,8 +54602,9 @@ function createAgentContextPack(input, db) {
|
|
|
54183
54602
|
description: truncate2(plan.description, limit.max_text_chars),
|
|
54184
54603
|
status: plan.status,
|
|
54185
54604
|
agent_id: plan.agent_id,
|
|
54186
|
-
tasks: planTasks.
|
|
54187
|
-
|
|
54605
|
+
tasks: planTasks.map(taskSummary).filter((item) => Boolean(item)),
|
|
54606
|
+
total_tasks: totalPlanTasks,
|
|
54607
|
+
omitted_tasks: Math.max(0, totalPlanTasks - planTasks.length)
|
|
54188
54608
|
} : null,
|
|
54189
54609
|
acceptance_criteria: acceptanceCriteria(task, limit.max_text_chars),
|
|
54190
54610
|
dependencies: {
|
|
@@ -54294,6 +54714,8 @@ ${pack.task.description}` : null,
|
|
|
54294
54714
|
"## Project And Plan",
|
|
54295
54715
|
pack.project ? `- Project: ${pack.project.name} (${pack.project.path})` : "- Project: none",
|
|
54296
54716
|
pack.plan ? `- Plan: ${pack.plan.name} (${pack.plan.status})` : "- Plan: none",
|
|
54717
|
+
pack.plan ? `- Plan tasks: ${pack.plan.tasks.length} shown of ${pack.plan.total_tasks} total (${pack.plan.omitted_tasks} omitted)` : null,
|
|
54718
|
+
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,
|
|
54297
54719
|
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,
|
|
54298
54720
|
"",
|
|
54299
54721
|
"## Acceptance Criteria",
|
|
@@ -54374,7 +54796,7 @@ function renderAgentContextPack(pack, format, compact2 = false) {
|
|
|
54374
54796
|
return compact2 ? renderAgentContextPackCompactMarkdown(pack) : renderAgentContextPackMarkdown(pack);
|
|
54375
54797
|
return compact2 ? JSON.stringify(pack) : JSON.stringify(pack, null, 2);
|
|
54376
54798
|
}
|
|
54377
|
-
var DEFAULT_LIMITS, ALL_CONTEXT_SECTIONS, BUDGET_TRIM_ORDER;
|
|
54799
|
+
var DEFAULT_LIMITS, MAX_CONTEXT_TASKS = 24, ALL_CONTEXT_SECTIONS, BUDGET_TRIM_ORDER;
|
|
54378
54800
|
var init_context_packs = __esm(() => {
|
|
54379
54801
|
init_comments();
|
|
54380
54802
|
init_database();
|
|
@@ -54393,7 +54815,7 @@ var init_context_packs = __esm(() => {
|
|
|
54393
54815
|
verification_limit: 10,
|
|
54394
54816
|
run_limit: 3,
|
|
54395
54817
|
dependency_limit: 12,
|
|
54396
|
-
plan_task_limit:
|
|
54818
|
+
plan_task_limit: 24,
|
|
54397
54819
|
max_text_chars: 6000,
|
|
54398
54820
|
summary_char_limit: 480,
|
|
54399
54821
|
stale_after_hours: 72
|
|
@@ -79216,7 +79638,7 @@ function parseObject2(value) {
|
|
|
79216
79638
|
function normalizeKey(value) {
|
|
79217
79639
|
return value.trim().toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
79218
79640
|
}
|
|
79219
|
-
function
|
|
79641
|
+
function normalizeTaskFindingFingerprint(value) {
|
|
79220
79642
|
const normalized = normalizeKey(value);
|
|
79221
79643
|
if (!normalized)
|
|
79222
79644
|
throw new Error("finding fingerprint is required");
|
|
@@ -79325,7 +79747,7 @@ function assertTask(taskId, db) {
|
|
|
79325
79747
|
throw new TaskNotFoundError(taskId);
|
|
79326
79748
|
}
|
|
79327
79749
|
function nextFinding(input, db) {
|
|
79328
|
-
const fingerprint2 =
|
|
79750
|
+
const fingerprint2 = normalizeTaskFindingFingerprint(input.fingerprint);
|
|
79329
79751
|
const title = redactOptional(input.title, 300);
|
|
79330
79752
|
if (!title)
|
|
79331
79753
|
throw new Error("finding title is required");
|
|
@@ -79473,7 +79895,7 @@ function resolveMissingTaskFindings(input, db) {
|
|
|
79473
79895
|
const timestamp4 = input.resolved_at || now();
|
|
79474
79896
|
const status = normalizeResolutionStatus(input.status);
|
|
79475
79897
|
const runId = resolveRunForTask(input.run_id, input.task_id, d);
|
|
79476
|
-
const present = new Set(input.fingerprints.map(
|
|
79898
|
+
const present = new Set(input.fingerprints.map(normalizeTaskFindingFingerprint));
|
|
79477
79899
|
const warnings = [];
|
|
79478
79900
|
const conditions = ["task_id = ?", "status = 'open'"];
|
|
79479
79901
|
const params = [input.task_id];
|
|
@@ -91218,6 +91640,15 @@ function formatError2(error3) {
|
|
|
91218
91640
|
if (error3 instanceof TaskNotFoundError) {
|
|
91219
91641
|
return JSON.stringify({ code: TaskNotFoundError.code, message: error3.message, suggestion: TaskNotFoundError.suggestion });
|
|
91220
91642
|
}
|
|
91643
|
+
if (error3 instanceof TaskReferenceAmbiguousError) {
|
|
91644
|
+
return JSON.stringify({
|
|
91645
|
+
code: TaskReferenceAmbiguousError.code,
|
|
91646
|
+
message: error3.message,
|
|
91647
|
+
candidate_project_ids: error3.candidateProjectIds,
|
|
91648
|
+
candidate_task_ids: error3.candidateTaskIds,
|
|
91649
|
+
suggestion: "Use a full task UUID."
|
|
91650
|
+
});
|
|
91651
|
+
}
|
|
91221
91652
|
if (error3 instanceof ProjectNotFoundError) {
|
|
91222
91653
|
return JSON.stringify({ code: ProjectNotFoundError.code, message: error3.message, suggestion: ProjectNotFoundError.suggestion });
|
|
91223
91654
|
}
|
|
@@ -92015,23 +92446,24 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
92015
92446
|
}
|
|
92016
92447
|
if ((path === "/health" || path === "/ready" || path === "/version") && method === "GET") {
|
|
92017
92448
|
const { getPackageVersion: getPackageVersion2 } = await Promise.resolve().then(() => (init_package_version(), exports_package_version));
|
|
92018
|
-
const {
|
|
92019
|
-
const
|
|
92449
|
+
const { isPostgresBackendConfigured: isPostgresBackendConfigured2, pingCloud: pingCloud2 } = await Promise.resolve().then(() => (init_cloud(), exports_cloud));
|
|
92450
|
+
const backend = isPostgresBackendConfigured2() ? "postgresql" : "sqlite";
|
|
92451
|
+
const mode = backend === "postgresql" ? "remote" : "local";
|
|
92020
92452
|
const version2 = getPackageVersion2();
|
|
92021
92453
|
if (path === "/version") {
|
|
92022
|
-
return Response.json({ status: "ok", version: version2, mode, name: "todos" });
|
|
92454
|
+
return Response.json({ status: "ok", version: version2, mode, backend, name: "todos" });
|
|
92023
92455
|
}
|
|
92024
92456
|
if (path === "/ready") {
|
|
92025
|
-
if (
|
|
92457
|
+
if (backend === "postgresql") {
|
|
92026
92458
|
try {
|
|
92027
92459
|
await pingCloud2();
|
|
92028
92460
|
} catch (e) {
|
|
92029
|
-
return Response.json({ status: "unavailable", version: version2, mode, error: e.message }, { status: 503 });
|
|
92461
|
+
return Response.json({ status: "unavailable", version: version2, mode, backend, error: e.message }, { status: 503 });
|
|
92030
92462
|
}
|
|
92031
92463
|
}
|
|
92032
|
-
return Response.json({ status: "ready", version: version2, mode });
|
|
92464
|
+
return Response.json({ status: "ready", version: version2, mode, backend });
|
|
92033
92465
|
}
|
|
92034
|
-
return Response.json({ status: "ok", version: version2, mode, name: "todos" });
|
|
92466
|
+
return Response.json({ status: "ok", version: version2, mode, backend, name: "todos" });
|
|
92035
92467
|
}
|
|
92036
92468
|
if ((path === "/openapi.json" || path === "/v1/openapi.json") && method === "GET") {
|
|
92037
92469
|
const { buildV1OpenApiDocument: buildV1OpenApiDocument2 } = await Promise.resolve().then(() => (init_openapi(), exports_openapi));
|