@hasna/todos 0.13.2 → 0.13.4
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 +100 -7
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/agent-commands.d.ts.map +1 -1
- package/dist/cli/commands/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 +1509 -786
- package/dist/cli/stage-a.d.ts.map +1 -1
- package/dist/contracts.js +118 -30
- package/dist/db/agent-names.d.ts +2 -1
- package/dist/db/agent-names.d.ts.map +1 -1
- 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 +584 -140
- package/dist/lib/agent-name-normalize.d.ts +17 -0
- package/dist/lib/agent-name-normalize.d.ts.map +1 -0
- 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 +554 -92
- 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 +560 -98
- 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 +623 -258
- 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.4",
|
|
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";
|
|
@@ -614,6 +657,11 @@ var init_types = __esm(() => {
|
|
|
614
657
|
};
|
|
615
658
|
});
|
|
616
659
|
|
|
660
|
+
// src/lib/agent-name-normalize.ts
|
|
661
|
+
function normalizeAgentNameInput(name) {
|
|
662
|
+
return name.trim().toLowerCase();
|
|
663
|
+
}
|
|
664
|
+
|
|
617
665
|
// src/lib/slugs.ts
|
|
618
666
|
function normalizeSlug(value) {
|
|
619
667
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
@@ -1086,6 +1134,185 @@ var init_postgres_sync = __esm(() => {
|
|
|
1086
1134
|
};
|
|
1087
1135
|
});
|
|
1088
1136
|
|
|
1137
|
+
// src/lib/integrity.ts
|
|
1138
|
+
function resolveIntegritySeverity(spec, measurement) {
|
|
1139
|
+
if (spec.kind === "dangling")
|
|
1140
|
+
return "error";
|
|
1141
|
+
if (spec.escalate_when_open && (measurement.open_count ?? 0) > 0)
|
|
1142
|
+
return "error";
|
|
1143
|
+
return spec.base_severity;
|
|
1144
|
+
}
|
|
1145
|
+
function plural(entity, count) {
|
|
1146
|
+
const [one, many] = ENTITY_LABEL[entity];
|
|
1147
|
+
return count === 1 ? one : many;
|
|
1148
|
+
}
|
|
1149
|
+
function formatIntegrityMessage(spec, measurement) {
|
|
1150
|
+
const noun = plural(spec.entity, measurement.count);
|
|
1151
|
+
const verb = measurement.count === 1 ? ["has", "references"] : ["have", "reference"];
|
|
1152
|
+
const open = measurement.open_count === null || measurement.open_count === 0 ? "" : ` (${measurement.open_count} still open)`;
|
|
1153
|
+
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}`;
|
|
1154
|
+
}
|
|
1155
|
+
function measuredCondition(spec, measurement, source) {
|
|
1156
|
+
return {
|
|
1157
|
+
id: spec.id,
|
|
1158
|
+
entity: spec.entity,
|
|
1159
|
+
field: spec.field,
|
|
1160
|
+
kind: spec.kind,
|
|
1161
|
+
count: measurement.count,
|
|
1162
|
+
open_count: measurement.open_count,
|
|
1163
|
+
severity: measurement.count > 0 ? resolveIntegritySeverity(spec, measurement) : null,
|
|
1164
|
+
verified: true,
|
|
1165
|
+
source,
|
|
1166
|
+
message: formatIntegrityMessage(spec, measurement),
|
|
1167
|
+
impact: spec.impact
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
function unverifiedCondition(spec, reason) {
|
|
1171
|
+
return {
|
|
1172
|
+
id: spec.id,
|
|
1173
|
+
entity: spec.entity,
|
|
1174
|
+
field: spec.field,
|
|
1175
|
+
kind: spec.kind,
|
|
1176
|
+
count: null,
|
|
1177
|
+
open_count: null,
|
|
1178
|
+
severity: null,
|
|
1179
|
+
verified: false,
|
|
1180
|
+
source: "unverified",
|
|
1181
|
+
unverified_reason: reason,
|
|
1182
|
+
message: `${spec.id}: NOT CHECKED \u2014 ${reason}`,
|
|
1183
|
+
impact: spec.impact
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
function summarizeIntegrity(conditions) {
|
|
1187
|
+
const measured = conditions.filter((condition) => condition.verified && condition.count !== null);
|
|
1188
|
+
const findings = measured.filter((condition) => (condition.count ?? 0) > 0);
|
|
1189
|
+
const unverified = conditions.length - measured.length;
|
|
1190
|
+
return {
|
|
1191
|
+
ok: unverified === 0 && findings.length === 0,
|
|
1192
|
+
findings: findings.length,
|
|
1193
|
+
rows: findings.reduce((total, condition) => total + (condition.count ?? 0), 0),
|
|
1194
|
+
errors: findings.filter((condition) => condition.severity === "error").length,
|
|
1195
|
+
warnings: findings.filter((condition) => condition.severity === "warn").length,
|
|
1196
|
+
unverified,
|
|
1197
|
+
complete: unverified === 0
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
function buildIntegrityReport(conditions, generatedAt) {
|
|
1201
|
+
const measuredSources = [...new Set(conditions.map((condition) => condition.source))].filter((source) => source !== "unverified");
|
|
1202
|
+
return {
|
|
1203
|
+
schema_version: TODOS_INTEGRITY_SCHEMA_VERSION,
|
|
1204
|
+
generated_at: generatedAt,
|
|
1205
|
+
source: measuredSources.length === 0 ? "unverified" : measuredSources.length === 1 ? measuredSources[0] : "remote-derived",
|
|
1206
|
+
conditions,
|
|
1207
|
+
summary: summarizeIntegrity(conditions)
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
function sqliteOpenStatusList() {
|
|
1211
|
+
return OPEN_TASK_STATUSES.map((status) => `'${status}'`).join(", ");
|
|
1212
|
+
}
|
|
1213
|
+
function buildSqliteIntegritySql(spec) {
|
|
1214
|
+
const table = SQLITE_TABLE[spec.entity];
|
|
1215
|
+
const target = SQLITE_TABLE[spec.target];
|
|
1216
|
+
const column = `t."${spec.field}"`;
|
|
1217
|
+
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})`;
|
|
1218
|
+
const openExpr = spec.entity === "task" ? `SUM(CASE WHEN t."status" IN (${sqliteOpenStatusList()}) THEN 1 ELSE 0 END)` : "NULL";
|
|
1219
|
+
return `SELECT COUNT(*) AS count, ${openExpr} AS open_count FROM "${table}" t WHERE ${predicate}`;
|
|
1220
|
+
}
|
|
1221
|
+
function buildPostgresIntegritySql(spec, options) {
|
|
1222
|
+
const params = [options.service, RECORD_TYPE[spec.entity]];
|
|
1223
|
+
const p = (value) => {
|
|
1224
|
+
params.push(value);
|
|
1225
|
+
return `$${params.length}`;
|
|
1226
|
+
};
|
|
1227
|
+
const column = `t.payload->>'${spec.field}'`;
|
|
1228
|
+
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})`;
|
|
1229
|
+
const openExpr = spec.entity === "task" ? `COUNT(*) FILTER (WHERE t.payload->>'status' IN (${OPEN_TASK_STATUSES.map((status) => p(status)).join(", ")}))::int` : "NULL::int";
|
|
1230
|
+
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}`;
|
|
1231
|
+
return { sql, params };
|
|
1232
|
+
}
|
|
1233
|
+
var TODOS_INTEGRITY_SCHEMA_VERSION = "todos.integrity.v1", TERMINAL_TASK_STATUSES, OPEN_TASK_STATUSES, INTEGRITY_CONDITIONS, ENTITY_LABEL, SQLITE_TABLE, RECORD_TYPE;
|
|
1234
|
+
var init_integrity = __esm(() => {
|
|
1235
|
+
init_types();
|
|
1236
|
+
TERMINAL_TASK_STATUSES = ["completed", "failed", "cancelled"];
|
|
1237
|
+
OPEN_TASK_STATUSES = TASK_STATUSES.filter((status) => !TERMINAL_TASK_STATUSES.includes(status));
|
|
1238
|
+
INTEGRITY_CONDITIONS = [
|
|
1239
|
+
{
|
|
1240
|
+
id: "tasks_without_project",
|
|
1241
|
+
entity: "task",
|
|
1242
|
+
field: "project_id",
|
|
1243
|
+
target: "project",
|
|
1244
|
+
kind: "missing",
|
|
1245
|
+
base_severity: "warn",
|
|
1246
|
+
escalate_when_open: true,
|
|
1247
|
+
impact: "invisible to every project-scoped read (list, status, next, claim)"
|
|
1248
|
+
},
|
|
1249
|
+
{
|
|
1250
|
+
id: "tasks_without_task_list",
|
|
1251
|
+
entity: "task",
|
|
1252
|
+
field: "task_list_id",
|
|
1253
|
+
target: "task_list",
|
|
1254
|
+
kind: "missing",
|
|
1255
|
+
base_severity: "warn",
|
|
1256
|
+
escalate_when_open: true,
|
|
1257
|
+
impact: "invisible to every task-list read \u2014 the list reports zero open work"
|
|
1258
|
+
},
|
|
1259
|
+
{
|
|
1260
|
+
id: "tasks_with_unregistered_project",
|
|
1261
|
+
entity: "task",
|
|
1262
|
+
field: "project_id",
|
|
1263
|
+
target: "project",
|
|
1264
|
+
kind: "dangling",
|
|
1265
|
+
base_severity: "error",
|
|
1266
|
+
escalate_when_open: false,
|
|
1267
|
+
impact: "points at a project id that does not exist; the reference can never resolve"
|
|
1268
|
+
},
|
|
1269
|
+
{
|
|
1270
|
+
id: "tasks_with_unregistered_task_list",
|
|
1271
|
+
entity: "task",
|
|
1272
|
+
field: "task_list_id",
|
|
1273
|
+
target: "task_list",
|
|
1274
|
+
kind: "dangling",
|
|
1275
|
+
base_severity: "error",
|
|
1276
|
+
escalate_when_open: false,
|
|
1277
|
+
impact: "points at a task-list id that does not exist; the reference can never resolve"
|
|
1278
|
+
},
|
|
1279
|
+
{
|
|
1280
|
+
id: "task_lists_without_project",
|
|
1281
|
+
entity: "task_list",
|
|
1282
|
+
field: "project_id",
|
|
1283
|
+
target: "project",
|
|
1284
|
+
kind: "missing",
|
|
1285
|
+
base_severity: "warn",
|
|
1286
|
+
escalate_when_open: false,
|
|
1287
|
+
impact: "unbound list \u2014 unreachable from any project, so its tasks are unroutable"
|
|
1288
|
+
},
|
|
1289
|
+
{
|
|
1290
|
+
id: "task_lists_with_unregistered_project",
|
|
1291
|
+
entity: "task_list",
|
|
1292
|
+
field: "project_id",
|
|
1293
|
+
target: "project",
|
|
1294
|
+
kind: "dangling",
|
|
1295
|
+
base_severity: "error",
|
|
1296
|
+
escalate_when_open: false,
|
|
1297
|
+
impact: "points at a project id that does not exist; the list can never be reached"
|
|
1298
|
+
}
|
|
1299
|
+
];
|
|
1300
|
+
ENTITY_LABEL = {
|
|
1301
|
+
task: ["task", "tasks"],
|
|
1302
|
+
task_list: ["task list", "task lists"]
|
|
1303
|
+
};
|
|
1304
|
+
SQLITE_TABLE = {
|
|
1305
|
+
task: "tasks",
|
|
1306
|
+
task_list: "task_lists",
|
|
1307
|
+
project: "projects"
|
|
1308
|
+
};
|
|
1309
|
+
RECORD_TYPE = {
|
|
1310
|
+
task: "tasks",
|
|
1311
|
+
task_list: "task_lists",
|
|
1312
|
+
project: "projects"
|
|
1313
|
+
};
|
|
1314
|
+
});
|
|
1315
|
+
|
|
1089
1316
|
// src/lib/sync-utils.ts
|
|
1090
1317
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
|
|
1091
1318
|
import { join } from "path";
|
|
@@ -1368,7 +1595,7 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
1368
1595
|
agents: {
|
|
1369
1596
|
register: (input, context) => registerAgent(input, store, context),
|
|
1370
1597
|
get: (id) => store.get("agents", id),
|
|
1371
|
-
getByName: async (name) => (await store.list("agents")
|
|
1598
|
+
getByName: async (name) => matchAgentByName(await store.list("agents"), name),
|
|
1372
1599
|
list: async (options2) => (await store.list("agents")).filter((agent) => options2?.include_archived || agent.status !== "archived").sort((a, b) => a.name.localeCompare(b.name)),
|
|
1373
1600
|
update: (id, input) => updateAgent(id, input, store),
|
|
1374
1601
|
heartbeat: (idOrName, context) => heartbeatAgent(idOrName, store, context),
|
|
@@ -1425,6 +1652,9 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
1425
1652
|
exportSnapshot: () => exportSnapshot(store),
|
|
1426
1653
|
importSnapshot: (snapshot, context) => importSnapshot(snapshot, store, context)
|
|
1427
1654
|
},
|
|
1655
|
+
integrity: {
|
|
1656
|
+
report: () => store.integrityReport()
|
|
1657
|
+
},
|
|
1428
1658
|
transaction: (fn) => fn(adapter)
|
|
1429
1659
|
};
|
|
1430
1660
|
return adapter;
|
|
@@ -1531,8 +1761,9 @@ class PostgresJsonRecordStore {
|
|
|
1531
1761
|
conds.push(`payload->>'agent_id' = ${p(filter.agent_id)}`);
|
|
1532
1762
|
if (filter.session_id !== undefined)
|
|
1533
1763
|
conds.push(`payload->>'session_id' = ${p(filter.session_id)}`);
|
|
1534
|
-
if (filter.tags?.length)
|
|
1535
|
-
conds.push(`payload->'tags'
|
|
1764
|
+
if (filter.tags?.length) {
|
|
1765
|
+
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)})`);
|
|
1766
|
+
}
|
|
1536
1767
|
if (filter.has_recurrence !== undefined) {
|
|
1537
1768
|
conds.push(`(COALESCE(payload->>'recurrence_rule', '') <> '') = ${p(filter.has_recurrence)}`);
|
|
1538
1769
|
}
|
|
@@ -1593,7 +1824,8 @@ class PostgresJsonRecordStore {
|
|
|
1593
1824
|
AND object_id COLLATE "C" >= $2 AND object_id COLLATE "C" < $3
|
|
1594
1825
|
LIMIT 2`, [this.service, raw, upper]);
|
|
1595
1826
|
if (prefixResult.rows.length > 1) {
|
|
1596
|
-
|
|
1827
|
+
const candidates = prefixResult.rows.map((row) => payloadRecord2(row.payload));
|
|
1828
|
+
throw new TaskReferenceAmbiguousError(ref, candidates.map((task) => ({ task_id: task.id, project_id: task.project_id })));
|
|
1597
1829
|
}
|
|
1598
1830
|
if (prefixResult.rows.length === 1) {
|
|
1599
1831
|
return payloadRecord2(prefixResult.rows[0].payload);
|
|
@@ -1604,7 +1836,8 @@ class PostgresJsonRecordStore {
|
|
|
1604
1836
|
AND LOWER(payload->>'short_id') = $2
|
|
1605
1837
|
LIMIT 2`, [this.service, raw]);
|
|
1606
1838
|
if (shortIdResult.rows.length > 1) {
|
|
1607
|
-
|
|
1839
|
+
const candidates = shortIdResult.rows.map((row) => payloadRecord2(row.payload));
|
|
1840
|
+
throw new TaskReferenceAmbiguousError(ref, candidates.map((task) => ({ task_id: task.id, project_id: task.project_id })));
|
|
1608
1841
|
}
|
|
1609
1842
|
if (shortIdResult.rows.length === 1) {
|
|
1610
1843
|
return payloadRecord2(shortIdResult.rows[0].payload);
|
|
@@ -1618,6 +1851,24 @@ class PostgresJsonRecordStore {
|
|
|
1618
1851
|
const result = await this.options.client.query(sql, params);
|
|
1619
1852
|
return Number(result.rows[0]?.count ?? 0);
|
|
1620
1853
|
}
|
|
1854
|
+
async integrityReport() {
|
|
1855
|
+
await this.ensureSchema();
|
|
1856
|
+
const conditions = [];
|
|
1857
|
+
for (const spec of INTEGRITY_CONDITIONS) {
|
|
1858
|
+
const { sql, params } = buildPostgresIntegritySql(spec, { table: this.tableName, service: this.service });
|
|
1859
|
+
try {
|
|
1860
|
+
const result = await this.options.client.query(sql, params);
|
|
1861
|
+
const row = result.rows[0];
|
|
1862
|
+
conditions.push(measuredCondition(spec, {
|
|
1863
|
+
count: Number(row?.count ?? 0),
|
|
1864
|
+
open_count: spec.entity === "task" ? Number(row?.open_count ?? 0) : null
|
|
1865
|
+
}, "postgres"));
|
|
1866
|
+
} catch (error) {
|
|
1867
|
+
conditions.push(unverifiedCondition(spec, error instanceof Error ? error.message : String(error)));
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
return buildIntegrityReport(conditions, new Date().toISOString());
|
|
1871
|
+
}
|
|
1621
1872
|
async listTombstones() {
|
|
1622
1873
|
await this.ensureSchema();
|
|
1623
1874
|
const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
|
|
@@ -2093,7 +2344,7 @@ async function startTask(id, agentId, store) {
|
|
|
2093
2344
|
if (task.status !== "pending" && task.status !== "in_progress") {
|
|
2094
2345
|
throw new Error(`Task is ${task.status} and cannot be started by ${agentId}`);
|
|
2095
2346
|
}
|
|
2096
|
-
|
|
2347
|
+
const started = await patchTask(task, {
|
|
2097
2348
|
status: "in_progress",
|
|
2098
2349
|
assigned_to: task.assigned_to ?? agentId,
|
|
2099
2350
|
agent_id: task.agent_id ?? agentId,
|
|
@@ -2101,6 +2352,8 @@ async function startTask(id, agentId, store) {
|
|
|
2101
2352
|
locked_at: new Date().toISOString(),
|
|
2102
2353
|
started_at: task.started_at ?? new Date().toISOString()
|
|
2103
2354
|
}, store);
|
|
2355
|
+
await logTaskChange(task.id, "start", "status", task.status, "in_progress", agentId, store);
|
|
2356
|
+
return started;
|
|
2104
2357
|
}
|
|
2105
2358
|
async function completeTask(id, agentId, options, store) {
|
|
2106
2359
|
const task = await store.completeTask(id, agentId, options);
|
|
@@ -2403,15 +2656,27 @@ async function updatePlan(id, input, store) {
|
|
|
2403
2656
|
}
|
|
2404
2657
|
return store.upsert("plans", { ...plan, ...patch, updated_at: new Date().toISOString() });
|
|
2405
2658
|
}
|
|
2659
|
+
function matchAgentByName(agents, name, options) {
|
|
2660
|
+
const target = normalizeAgentNameInput(name);
|
|
2661
|
+
if (!target)
|
|
2662
|
+
return null;
|
|
2663
|
+
const matches = agents.filter((agent) => normalizeAgentNameInput(agent.name) === target && (options?.includeArchived !== false || agent.status !== "archived"));
|
|
2664
|
+
if (matches.length === 0)
|
|
2665
|
+
return null;
|
|
2666
|
+
return matches.reduce((freshest, candidate) => new Date(candidate.last_seen_at).getTime() > new Date(freshest.last_seen_at).getTime() ? candidate : freshest);
|
|
2667
|
+
}
|
|
2406
2668
|
async function registerAgent(input, store, context) {
|
|
2407
|
-
const
|
|
2669
|
+
const canonicalName = normalizeAgentNameInput(input.name);
|
|
2670
|
+
const existing = matchAgentByName(await store.list("agents"), canonicalName, {
|
|
2671
|
+
includeArchived: false
|
|
2672
|
+
});
|
|
2408
2673
|
if (existing && !input.force && existing.session_id && existing.session_id !== input.session_id) {
|
|
2409
|
-
return { conflict: true, message: `Agent name '${
|
|
2674
|
+
return { conflict: true, message: `Agent name '${canonicalName}' is already active` };
|
|
2410
2675
|
}
|
|
2411
2676
|
const timestamp = new Date().toISOString();
|
|
2412
2677
|
const agent = {
|
|
2413
2678
|
id: existing?.id ?? randomUUID().slice(0, 8),
|
|
2414
|
-
name:
|
|
2679
|
+
name: canonicalName,
|
|
2415
2680
|
description: input.description ?? existing?.description ?? null,
|
|
2416
2681
|
role: input.role ?? existing?.role ?? null,
|
|
2417
2682
|
title: input.title ?? existing?.title ?? null,
|
|
@@ -2449,7 +2714,7 @@ async function resolveAgent(idOrName, store) {
|
|
|
2449
2714
|
const byId = await store.get("agents", idOrName);
|
|
2450
2715
|
if (byId)
|
|
2451
2716
|
return byId;
|
|
2452
|
-
return (await store.list("agents")
|
|
2717
|
+
return matchAgentByName(await store.list("agents"), idOrName);
|
|
2453
2718
|
}
|
|
2454
2719
|
async function heartbeatAgent(idOrName, store, context) {
|
|
2455
2720
|
const agent = await resolveAgent(idOrName, store);
|
|
@@ -2767,6 +3032,7 @@ var CLOUD_LOCK_EXPIRY_MINUTES = 30, TASK_ORDER_TIEBREAK = "CASE payload->>'prior
|
|
|
2767
3032
|
var init_postgres_adapter = __esm(() => {
|
|
2768
3033
|
init_types();
|
|
2769
3034
|
init_postgres_sync();
|
|
3035
|
+
init_integrity();
|
|
2770
3036
|
init_redaction();
|
|
2771
3037
|
TASK_ORDER_BY = `ORDER BY ${TASK_ORDER_TIEBREAK}`;
|
|
2772
3038
|
});
|
|
@@ -4765,6 +5031,7 @@ __export(exports_cloud, {
|
|
|
4765
5031
|
resolveCloudDatabaseUrl: () => resolveCloudDatabaseUrl,
|
|
4766
5032
|
pingCloud: () => pingCloud,
|
|
4767
5033
|
normalizeCloudPayloads: () => normalizeCloudPayloads,
|
|
5034
|
+
isPostgresBackendConfigured: () => isPostgresBackendConfigured,
|
|
4768
5035
|
isCloudModeEnabled: () => isCloudModeEnabled,
|
|
4769
5036
|
getCloudVerifier: () => getCloudVerifier,
|
|
4770
5037
|
getCloudStorageAdapter: () => getCloudStorageAdapter,
|
|
@@ -4787,9 +5054,12 @@ function resolveCloudDatabaseUrl(env = process.env) {
|
|
|
4787
5054
|
function resolveSigningSecret(env = process.env) {
|
|
4788
5055
|
return env.HASNA_TODOS_API_SIGNING_KEY || env.HASNA_API_SIGNING_KEY || env.API_KEY_SIGNING_SECRET || undefined;
|
|
4789
5056
|
}
|
|
4790
|
-
function
|
|
5057
|
+
function isPostgresBackendConfigured(env = process.env) {
|
|
4791
5058
|
return Boolean(resolveCloudDatabaseUrl(env));
|
|
4792
5059
|
}
|
|
5060
|
+
function isCloudModeEnabled(env = process.env) {
|
|
5061
|
+
return isPostgresBackendConfigured(env);
|
|
5062
|
+
}
|
|
4793
5063
|
function getClient() {
|
|
4794
5064
|
if (cachedClient)
|
|
4795
5065
|
return cachedClient;
|
|
@@ -8617,6 +8887,30 @@ function findGitRoot(startDir) {
|
|
|
8617
8887
|
}
|
|
8618
8888
|
return null;
|
|
8619
8889
|
}
|
|
8890
|
+
function getGlobalDbPath() {
|
|
8891
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
8892
|
+
return join3(home, ".hasna", "todos", "todos.db");
|
|
8893
|
+
}
|
|
8894
|
+
function hasExplicitProjectArg(args = process.argv.slice(2)) {
|
|
8895
|
+
return args.some((arg) => arg === "--project" || arg.startsWith("--project="));
|
|
8896
|
+
}
|
|
8897
|
+
function getCliCommand(args = process.argv.slice(2)) {
|
|
8898
|
+
const globalOptionsWithValues = new Set(["--project", "--agent", "--session"]);
|
|
8899
|
+
for (let index = 0;index < args.length; index += 1) {
|
|
8900
|
+
const arg = args[index];
|
|
8901
|
+
if (globalOptionsWithValues.has(arg)) {
|
|
8902
|
+
index += 1;
|
|
8903
|
+
continue;
|
|
8904
|
+
}
|
|
8905
|
+
if (arg.startsWith("-"))
|
|
8906
|
+
continue;
|
|
8907
|
+
return arg;
|
|
8908
|
+
}
|
|
8909
|
+
return null;
|
|
8910
|
+
}
|
|
8911
|
+
function canCreateScopedProjectDb(args = process.argv.slice(2)) {
|
|
8912
|
+
return getCliCommand(args) === "project-bootstrap" && !args.some((arg) => arg === "--dry-run" || arg.startsWith("--dry-run="));
|
|
8913
|
+
}
|
|
8620
8914
|
function getDbPath() {
|
|
8621
8915
|
if (process.env["HASNA_TODOS_DB_PATH"]) {
|
|
8622
8916
|
return process.env["HASNA_TODOS_DB_PATH"];
|
|
@@ -8624,18 +8918,19 @@ function getDbPath() {
|
|
|
8624
8918
|
if (process.env["TODOS_DB_PATH"]) {
|
|
8625
8919
|
return process.env["TODOS_DB_PATH"];
|
|
8626
8920
|
}
|
|
8921
|
+
if (hasExplicitProjectArg())
|
|
8922
|
+
return getGlobalDbPath();
|
|
8627
8923
|
const cwd = process.cwd();
|
|
8628
8924
|
const nearest = findNearestProjectDb(cwd);
|
|
8629
8925
|
if (nearest)
|
|
8630
8926
|
return nearest;
|
|
8631
8927
|
if (process.env["TODOS_DB_SCOPE"] === "project") {
|
|
8632
8928
|
const gitRoot = findGitRoot(cwd);
|
|
8633
|
-
if (gitRoot) {
|
|
8929
|
+
if (gitRoot && canCreateScopedProjectDb()) {
|
|
8634
8930
|
return join3(gitRoot, ".hasna", "todos", "todos.db");
|
|
8635
8931
|
}
|
|
8636
8932
|
}
|
|
8637
|
-
|
|
8638
|
-
return join3(home, ".hasna", "todos", "todos.db");
|
|
8933
|
+
return getGlobalDbPath();
|
|
8639
8934
|
}
|
|
8640
8935
|
function getDatabasePath() {
|
|
8641
8936
|
return getDbPath();
|
|
@@ -8738,10 +9033,13 @@ function resolvePartialId(db, table, partialId) {
|
|
|
8738
9033
|
return null;
|
|
8739
9034
|
}
|
|
8740
9035
|
if (table === "tasks") {
|
|
8741
|
-
const shortIdRows = db.query("SELECT id FROM tasks WHERE short_id = ?").all(partialId);
|
|
9036
|
+
const shortIdRows = db.query("SELECT id, project_id FROM tasks WHERE LOWER(short_id) = LOWER(?) ORDER BY project_id, id").all(partialId);
|
|
8742
9037
|
if (shortIdRows.length === 1) {
|
|
8743
9038
|
return shortIdRows[0].id;
|
|
8744
9039
|
}
|
|
9040
|
+
if (shortIdRows.length > 1) {
|
|
9041
|
+
throw new TaskReferenceAmbiguousError(partialId, shortIdRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
|
|
9042
|
+
}
|
|
8745
9043
|
}
|
|
8746
9044
|
if (table === "task_lists") {
|
|
8747
9045
|
const slugRow = db.query("SELECT id FROM task_lists WHERE slug = ?").get(partialId);
|
|
@@ -8905,7 +9203,7 @@ function authNotConfiguredMessage(host) {
|
|
|
8905
9203
|
`);
|
|
8906
9204
|
}
|
|
8907
9205
|
function resolveAuthPosture(input) {
|
|
8908
|
-
const hosted = input.hosted ??
|
|
9206
|
+
const hosted = input.hosted ?? isPostgresBackendConfigured();
|
|
8909
9207
|
const hasCredentialSource = Boolean(input.apiKey) || input.hasGeneratedKeys;
|
|
8910
9208
|
if (hasCredentialSource) {
|
|
8911
9209
|
return {
|
|
@@ -16413,9 +16711,6 @@ var init_tasks = __esm(() => {
|
|
|
16413
16711
|
});
|
|
16414
16712
|
|
|
16415
16713
|
// src/db/agent-names.ts
|
|
16416
|
-
function normalizeAgentNameInput(name) {
|
|
16417
|
-
return name.trim().toLowerCase();
|
|
16418
|
-
}
|
|
16419
16714
|
function hasGeneratedNumericSuffix(name) {
|
|
16420
16715
|
return NUMERIC_SUFFIX_RE.test(normalizeAgentNameInput(name));
|
|
16421
16716
|
}
|
|
@@ -17227,6 +17522,32 @@ var init_headless_boundaries = __esm(() => {
|
|
|
17227
17522
|
LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
17228
17523
|
});
|
|
17229
17524
|
|
|
17525
|
+
// src/db/integrity.ts
|
|
17526
|
+
function tableExists(db, table) {
|
|
17527
|
+
return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
|
|
17528
|
+
}
|
|
17529
|
+
function scanSqliteIntegrity(db = getDatabase()) {
|
|
17530
|
+
const missing = Object.keys(REQUIRED_TABLES).filter((table) => !tableExists(db, table));
|
|
17531
|
+
const conditions = INTEGRITY_CONDITIONS.map((spec) => {
|
|
17532
|
+
if (missing.length > 0) {
|
|
17533
|
+
return unverifiedCondition(spec, `local schema is missing table(s): ${missing.join(", ")}`);
|
|
17534
|
+
}
|
|
17535
|
+
try {
|
|
17536
|
+
const row = db.query(buildSqliteIntegritySql(spec)).get();
|
|
17537
|
+
return measuredCondition(spec, { count: Number(row?.count ?? 0), open_count: spec.entity === "task" ? Number(row?.open_count ?? 0) : null }, "sqlite");
|
|
17538
|
+
} catch (error) {
|
|
17539
|
+
return unverifiedCondition(spec, error instanceof Error ? error.message : String(error));
|
|
17540
|
+
}
|
|
17541
|
+
});
|
|
17542
|
+
return buildIntegrityReport(conditions, now());
|
|
17543
|
+
}
|
|
17544
|
+
var REQUIRED_TABLES;
|
|
17545
|
+
var init_integrity2 = __esm(() => {
|
|
17546
|
+
init_database();
|
|
17547
|
+
init_integrity();
|
|
17548
|
+
REQUIRED_TABLES = { tasks: true, task_lists: true, projects: true };
|
|
17549
|
+
});
|
|
17550
|
+
|
|
17230
17551
|
// src/lib/doctor.ts
|
|
17231
17552
|
var exports_doctor = {};
|
|
17232
17553
|
__export(exports_doctor, {
|
|
@@ -17234,7 +17555,7 @@ __export(exports_doctor, {
|
|
|
17234
17555
|
});
|
|
17235
17556
|
import { chmodSync, copyFileSync, existsSync as existsSync7, mkdirSync as mkdirSync5, statSync as statSync3 } from "fs";
|
|
17236
17557
|
import { basename as basename2, dirname as dirname5, join as join6 } from "path";
|
|
17237
|
-
function
|
|
17558
|
+
function tableExists2(db, table) {
|
|
17238
17559
|
return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
|
|
17239
17560
|
}
|
|
17240
17561
|
function quoteIdent(identifier) {
|
|
@@ -17318,7 +17639,7 @@ function findDuplicateIndexes(db) {
|
|
|
17318
17639
|
return duplicates;
|
|
17319
17640
|
}
|
|
17320
17641
|
function findMissingProjectRoots(db) {
|
|
17321
|
-
if (!
|
|
17642
|
+
if (!tableExists2(db, "projects"))
|
|
17322
17643
|
return 0;
|
|
17323
17644
|
let missing = 0;
|
|
17324
17645
|
const rows = db.query("SELECT path FROM projects WHERE path IS NOT NULL AND path != ''").all();
|
|
@@ -17333,7 +17654,7 @@ function findMissingProjectRoots(db) {
|
|
|
17333
17654
|
return missing;
|
|
17334
17655
|
}
|
|
17335
17656
|
function addTaskStateChecks(db, checks) {
|
|
17336
|
-
if (!
|
|
17657
|
+
if (!tableExists2(db, "tasks"))
|
|
17337
17658
|
return;
|
|
17338
17659
|
const columns = getTableColumns(db, "tasks");
|
|
17339
17660
|
const staleCutoff = new Date(Date.now() - 30 * 60 * 1000).toISOString();
|
|
@@ -17405,15 +17726,42 @@ function createBackup(dbPath) {
|
|
|
17405
17726
|
function pushRepair(repairs, type, message, applied, count) {
|
|
17406
17727
|
repairs.push({ type, message, applied, count });
|
|
17407
17728
|
}
|
|
17408
|
-
function summarize2(checks, repairs) {
|
|
17729
|
+
function summarize2(checks, repairs, integrity) {
|
|
17409
17730
|
return {
|
|
17410
17731
|
errors: checks.filter((check) => check.severity === "error").length,
|
|
17411
17732
|
warnings: checks.filter((check) => check.severity === "warn").length,
|
|
17412
17733
|
infos: checks.filter((check) => check.severity === "info").length,
|
|
17413
17734
|
repairable: checks.filter((check) => check.repairable).length,
|
|
17414
|
-
applied: repairs.filter((repair) => repair.applied).length
|
|
17735
|
+
applied: repairs.filter((repair) => repair.applied).length,
|
|
17736
|
+
integrity_findings: integrity.summary.findings,
|
|
17737
|
+
integrity_rows: integrity.summary.rows,
|
|
17738
|
+
integrity_unverified: integrity.summary.unverified
|
|
17415
17739
|
};
|
|
17416
17740
|
}
|
|
17741
|
+
function addIntegrityChecks(integrity, checks) {
|
|
17742
|
+
for (const condition of integrity.conditions) {
|
|
17743
|
+
if (!condition.verified) {
|
|
17744
|
+
addCheck(checks, {
|
|
17745
|
+
severity: "warn",
|
|
17746
|
+
type: `${condition.id}_unverified`,
|
|
17747
|
+
message: condition.message,
|
|
17748
|
+
repairable: false,
|
|
17749
|
+
integrity: true
|
|
17750
|
+
});
|
|
17751
|
+
continue;
|
|
17752
|
+
}
|
|
17753
|
+
if ((condition.count ?? 0) === 0)
|
|
17754
|
+
continue;
|
|
17755
|
+
addCheck(checks, {
|
|
17756
|
+
severity: condition.severity ?? "warn",
|
|
17757
|
+
type: condition.id,
|
|
17758
|
+
message: `${condition.message} \u2014 ${condition.impact}`,
|
|
17759
|
+
count: condition.count ?? undefined,
|
|
17760
|
+
repairable: false,
|
|
17761
|
+
integrity: true
|
|
17762
|
+
});
|
|
17763
|
+
}
|
|
17764
|
+
}
|
|
17417
17765
|
function deleteOrphans(db, table, where) {
|
|
17418
17766
|
const before = countQuery(db, `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`);
|
|
17419
17767
|
if (before > 0)
|
|
@@ -17442,7 +17790,7 @@ function runTodosDoctor(options = {}) {
|
|
|
17442
17790
|
message: `Schema at migration ${migrationCurrent}`
|
|
17443
17791
|
});
|
|
17444
17792
|
}
|
|
17445
|
-
const missingTables =
|
|
17793
|
+
const missingTables = REQUIRED_TABLES2.filter((table) => !tableExists2(db, table));
|
|
17446
17794
|
if (missingTables.length > 0) {
|
|
17447
17795
|
addCheck(checks, {
|
|
17448
17796
|
severity: "error",
|
|
@@ -17452,7 +17800,7 @@ function runTodosDoctor(options = {}) {
|
|
|
17452
17800
|
repairable: true
|
|
17453
17801
|
});
|
|
17454
17802
|
}
|
|
17455
|
-
const orphanedParents =
|
|
17803
|
+
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
17804
|
if (orphanedParents > 0) {
|
|
17457
17805
|
addCheck(checks, {
|
|
17458
17806
|
severity: "error",
|
|
@@ -17462,7 +17810,7 @@ function runTodosDoctor(options = {}) {
|
|
|
17462
17810
|
repairable: true
|
|
17463
17811
|
});
|
|
17464
17812
|
}
|
|
17465
|
-
const orphanedDependencies =
|
|
17813
|
+
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
17814
|
if (orphanedDependencies > 0) {
|
|
17467
17815
|
addCheck(checks, {
|
|
17468
17816
|
severity: "error",
|
|
@@ -17481,7 +17829,7 @@ function runTodosDoctor(options = {}) {
|
|
|
17481
17829
|
];
|
|
17482
17830
|
let orphanedRows = 0;
|
|
17483
17831
|
for (const [table, where] of orphanTables) {
|
|
17484
|
-
if (!
|
|
17832
|
+
if (!tableExists2(db, table))
|
|
17485
17833
|
continue;
|
|
17486
17834
|
orphanedRows += countQuery(db, `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`);
|
|
17487
17835
|
}
|
|
@@ -17495,6 +17843,8 @@ function runTodosDoctor(options = {}) {
|
|
|
17495
17843
|
});
|
|
17496
17844
|
}
|
|
17497
17845
|
addTaskStateChecks(db, checks);
|
|
17846
|
+
const integrity = scanSqliteIntegrity(db);
|
|
17847
|
+
addIntegrityChecks(integrity, checks);
|
|
17498
17848
|
const corruptJson = findCorruptJsonMetadata(db);
|
|
17499
17849
|
if (corruptJson.length > 0) {
|
|
17500
17850
|
addCheck(checks, {
|
|
@@ -17553,12 +17903,12 @@ function runTodosDoctor(options = {}) {
|
|
|
17553
17903
|
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
17904
|
pushRepair(repairs, "orphaned_task_parents", "Cleared missing parent references", true, orphanedParents);
|
|
17555
17905
|
}
|
|
17556
|
-
if (orphanedDependencies > 0 &&
|
|
17906
|
+
if (orphanedDependencies > 0 && tableExists2(db, "task_dependencies")) {
|
|
17557
17907
|
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
17908
|
pushRepair(repairs, "orphaned_task_dependencies", "Deleted dependency rows referencing missing tasks", true, count);
|
|
17559
17909
|
}
|
|
17560
17910
|
for (const [table, where] of orphanTables) {
|
|
17561
|
-
if (!
|
|
17911
|
+
if (!tableExists2(db, table))
|
|
17562
17912
|
continue;
|
|
17563
17913
|
const count = deleteOrphans(db, table, where);
|
|
17564
17914
|
if (count > 0)
|
|
@@ -17587,26 +17937,30 @@ function runTodosDoctor(options = {}) {
|
|
|
17587
17937
|
}
|
|
17588
17938
|
}
|
|
17589
17939
|
}
|
|
17590
|
-
const
|
|
17591
|
-
const
|
|
17940
|
+
const reread = apply && hasRepairableIssue ? runTodosDoctor({ db, dbPath, apply: false }) : undefined;
|
|
17941
|
+
const finalChecks = reread?.checks ?? checks;
|
|
17942
|
+
const finalIntegrity = reread?.integrity ?? integrity;
|
|
17943
|
+
const summary = summarize2(finalChecks, repairs, finalIntegrity);
|
|
17592
17944
|
return {
|
|
17593
|
-
ok: !finalChecks.some((check) => check.severity === "error"),
|
|
17945
|
+
ok: !finalChecks.some((check) => check.severity === "error") && finalIntegrity.summary.ok,
|
|
17594
17946
|
dry_run: !apply,
|
|
17595
17947
|
database_path: dbPath,
|
|
17596
17948
|
migration: { current: getMigrationLevel(db), expected: migrationExpected },
|
|
17597
17949
|
backup,
|
|
17598
17950
|
checks: finalChecks,
|
|
17599
17951
|
repairs,
|
|
17952
|
+
integrity: finalIntegrity,
|
|
17600
17953
|
summary
|
|
17601
17954
|
};
|
|
17602
17955
|
}
|
|
17603
|
-
var
|
|
17956
|
+
var REQUIRED_TABLES2;
|
|
17604
17957
|
var init_doctor = __esm(() => {
|
|
17605
17958
|
init_database();
|
|
17959
|
+
init_integrity2();
|
|
17606
17960
|
init_migrations();
|
|
17607
17961
|
init_schema();
|
|
17608
17962
|
init_recurrence();
|
|
17609
|
-
|
|
17963
|
+
REQUIRED_TABLES2 = [
|
|
17610
17964
|
"_migrations",
|
|
17611
17965
|
"projects",
|
|
17612
17966
|
"tasks",
|
|
@@ -18998,15 +19352,15 @@ function resolveTaskRefLocal(db, ref) {
|
|
|
18998
19352
|
return null;
|
|
18999
19353
|
if (TASK_UUID_RE.test(raw))
|
|
19000
19354
|
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}`)}%`);
|
|
19355
|
+
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
19356
|
if (prefixRows.length > 1) {
|
|
19003
|
-
throw new
|
|
19357
|
+
throw new TaskReferenceAmbiguousError(ref, prefixRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
|
|
19004
19358
|
}
|
|
19005
19359
|
if (prefixRows.length === 1)
|
|
19006
19360
|
return getTask(prefixRows[0].id, db);
|
|
19007
|
-
const shortIdRows = db.query("SELECT id FROM tasks WHERE LOWER(short_id) = ? LIMIT 2").all(raw);
|
|
19361
|
+
const shortIdRows = db.query("SELECT id, project_id FROM tasks WHERE LOWER(short_id) = ? ORDER BY project_id, id LIMIT 2").all(raw);
|
|
19008
19362
|
if (shortIdRows.length > 1) {
|
|
19009
|
-
throw new
|
|
19363
|
+
throw new TaskReferenceAmbiguousError(ref, shortIdRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
|
|
19010
19364
|
}
|
|
19011
19365
|
if (shortIdRows.length === 1)
|
|
19012
19366
|
return getTask(shortIdRows[0].id, db);
|
|
@@ -19155,6 +19509,9 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
19155
19509
|
exportSnapshot: () => exportSqliteTodosStorageSnapshot(database()),
|
|
19156
19510
|
importSnapshot: (snapshot) => importSqliteTodosStorageSnapshot(snapshot, database())
|
|
19157
19511
|
},
|
|
19512
|
+
integrity: {
|
|
19513
|
+
report: () => scanSqliteIntegrity(database())
|
|
19514
|
+
},
|
|
19158
19515
|
transaction: (fn) => {
|
|
19159
19516
|
const tx = database().transaction(() => fn(adapter));
|
|
19160
19517
|
return tx();
|
|
@@ -19164,6 +19521,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
19164
19521
|
}
|
|
19165
19522
|
var TASK_UUID_RE;
|
|
19166
19523
|
var init_local_sqlite = __esm(() => {
|
|
19524
|
+
init_types();
|
|
19167
19525
|
init_search();
|
|
19168
19526
|
init_tasks();
|
|
19169
19527
|
init_projects();
|
|
@@ -19174,6 +19532,7 @@ var init_local_sqlite = __esm(() => {
|
|
|
19174
19532
|
init_audit();
|
|
19175
19533
|
init_comments();
|
|
19176
19534
|
init_database();
|
|
19535
|
+
init_integrity2();
|
|
19177
19536
|
init_sqlite_snapshot();
|
|
19178
19537
|
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
19538
|
});
|
|
@@ -20472,6 +20831,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
20472
20831
|
{ name: "task_list_id", in: "query", schema: { type: "string" } },
|
|
20473
20832
|
{ name: "assigned_to", in: "query", schema: { type: "string" } },
|
|
20474
20833
|
{ name: "agent_id", in: "query", schema: { type: "string" } },
|
|
20834
|
+
{ name: "tags", in: "query", schema: { type: "string" }, description: "Comma-separated tags; matches tasks carrying any of them" },
|
|
20475
20835
|
{ name: "limit", in: "query", schema: { type: "integer", minimum: 1 } },
|
|
20476
20836
|
{ name: "offset", in: "query", schema: { type: "integer", minimum: 0 } }
|
|
20477
20837
|
],
|
|
@@ -21676,6 +22036,9 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
21676
22036
|
...url.searchParams.get("task_list_id") ? { task_list_id: url.searchParams.get("task_list_id") } : {},
|
|
21677
22037
|
...url.searchParams.get("assigned_to") ? { assigned_to: url.searchParams.get("assigned_to") } : {},
|
|
21678
22038
|
...url.searchParams.get("agent_id") ? { agent_id: url.searchParams.get("agent_id") } : {},
|
|
22039
|
+
...url.searchParams.get("tags") ? {
|
|
22040
|
+
tags: url.searchParams.get("tags").split(",").map((tag) => tag.trim()).filter(Boolean)
|
|
22041
|
+
} : {},
|
|
21679
22042
|
...url.searchParams.get("limit") ? { limit: Number(url.searchParams.get("limit")) } : {},
|
|
21680
22043
|
...url.searchParams.get("offset") ? { offset: Number(url.searchParams.get("offset")) } : {}
|
|
21681
22044
|
};
|
|
@@ -21957,6 +22320,13 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
21957
22320
|
try {
|
|
21958
22321
|
task = await store.tasks.resolveRef(id);
|
|
21959
22322
|
} catch (e) {
|
|
22323
|
+
if (e instanceof TaskReferenceAmbiguousError) {
|
|
22324
|
+
return error(409, e.message, {
|
|
22325
|
+
code: TaskReferenceAmbiguousError.code,
|
|
22326
|
+
candidate_project_ids: e.candidateProjectIds,
|
|
22327
|
+
candidate_task_ids: e.candidateTaskIds
|
|
22328
|
+
});
|
|
22329
|
+
}
|
|
21960
22330
|
const msg = e.message || "";
|
|
21961
22331
|
if (/ambiguous/i.test(msg))
|
|
21962
22332
|
return error(409, msg);
|
|
@@ -22314,6 +22684,15 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
22314
22684
|
]);
|
|
22315
22685
|
return json3({ tasks, tasks_all: tasksAll, subtasks: tasksAll - tasks, projects: projects.length });
|
|
22316
22686
|
}
|
|
22687
|
+
if (resource === "integrity" && !id) {
|
|
22688
|
+
if (method !== "GET")
|
|
22689
|
+
return error(405, `method ${method} not allowed on /v1/integrity`);
|
|
22690
|
+
if (typeof store.integrity?.report !== "function") {
|
|
22691
|
+
return error(501, "referential-integrity reporting is not supported by this storage backend");
|
|
22692
|
+
}
|
|
22693
|
+
const integrity = await store.integrity.report();
|
|
22694
|
+
return json3({ integrity });
|
|
22695
|
+
}
|
|
22317
22696
|
if (resource === "import") {
|
|
22318
22697
|
if (method !== "POST")
|
|
22319
22698
|
return error(405, `method ${method} not allowed on /v1/import`);
|
|
@@ -22333,6 +22712,13 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
22333
22712
|
}
|
|
22334
22713
|
return error(404, `unknown /v1 resource: ${resource ?? "(root)"}`);
|
|
22335
22714
|
} catch (e) {
|
|
22715
|
+
if (e instanceof TaskReferenceAmbiguousError) {
|
|
22716
|
+
return error(409, e.message, {
|
|
22717
|
+
code: TaskReferenceAmbiguousError.code,
|
|
22718
|
+
candidate_project_ids: e.candidateProjectIds,
|
|
22719
|
+
candidate_task_ids: e.candidateTaskIds
|
|
22720
|
+
});
|
|
22721
|
+
}
|
|
22336
22722
|
if (e instanceof LockError)
|
|
22337
22723
|
return error(409, e.message, { code: LockError.code });
|
|
22338
22724
|
if (e instanceof ResourceConflictError)
|
|
@@ -44660,14 +45046,12 @@ var init_http_client = __esm(() => {
|
|
|
44660
45046
|
|
|
44661
45047
|
// src/cli/cloud-router.ts
|
|
44662
45048
|
import { resolveStorageClient } from "@hasna/contracts/client/storage";
|
|
45049
|
+
import { normalizeStorageMode } from "@hasna/contracts/mode";
|
|
44663
45050
|
import { resolve as resolvePath } from "path";
|
|
44664
45051
|
function cleanMode(value) {
|
|
44665
45052
|
const normalized = value?.trim().toLowerCase();
|
|
44666
45053
|
return normalized || null;
|
|
44667
45054
|
}
|
|
44668
|
-
function modeRole(mode) {
|
|
44669
|
-
return mode === "local" ? "local" : "remote";
|
|
44670
|
-
}
|
|
44671
45055
|
function resolveTodosCliStorageMode(env = process.env) {
|
|
44672
45056
|
for (const source of ["HASNA_TODOS_STORAGE_MODE", "TODOS_STORAGE_MODE"]) {
|
|
44673
45057
|
if (env[source] !== undefined && env[source].trim() === "") {
|
|
@@ -44680,22 +45064,25 @@ function resolveTodosCliStorageMode(env = process.env) {
|
|
|
44680
45064
|
["HASNA_TODOS_STORAGE_MODE", canonical],
|
|
44681
45065
|
["TODOS_STORAGE_MODE", fallback]
|
|
44682
45066
|
]) {
|
|
44683
|
-
if (value && !
|
|
44684
|
-
throw new Error(`REMOTE_STORAGE_MODE_INVALID: ${source}=${value} must be local
|
|
45067
|
+
if (value && !(value in TRANSPORT_TOKENS)) {
|
|
45068
|
+
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
45069
|
}
|
|
44686
45070
|
}
|
|
44687
|
-
|
|
45071
|
+
const canonicalTransport = canonical ? TRANSPORT_TOKENS[canonical] : null;
|
|
45072
|
+
const fallbackTransport = fallback ? TRANSPORT_TOKENS[fallback] : null;
|
|
45073
|
+
if (canonicalTransport && fallbackTransport && canonicalTransport !== fallbackTransport) {
|
|
44688
45074
|
throw new Error(`REMOTE_STORAGE_MODE_CONFLICT: HASNA_TODOS_STORAGE_MODE=${canonical} conflicts with ` + `TODOS_STORAGE_MODE=${fallback}; local SQLite fallback is disabled`);
|
|
44689
45075
|
}
|
|
44690
|
-
const
|
|
45076
|
+
const transport = canonicalTransport ?? fallbackTransport ?? "sqlite";
|
|
44691
45077
|
return {
|
|
44692
|
-
mode,
|
|
44693
|
-
|
|
45078
|
+
mode: transport,
|
|
45079
|
+
transport,
|
|
45080
|
+
selected: transport === "http",
|
|
44694
45081
|
source: canonical ? "HASNA_TODOS_STORAGE_MODE" : fallback ? "TODOS_STORAGE_MODE" : "default"
|
|
44695
45082
|
};
|
|
44696
45083
|
}
|
|
44697
|
-
function
|
|
44698
|
-
return resolveTodosCliStorageMode(env).
|
|
45084
|
+
function requestedTransport(env) {
|
|
45085
|
+
return resolveTodosCliStorageMode(env).transport;
|
|
44699
45086
|
}
|
|
44700
45087
|
function normalizeRemoteAuthorityUrl(value) {
|
|
44701
45088
|
const trimmed = value?.trim();
|
|
@@ -44748,7 +45135,7 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env) {
|
|
|
44748
45135
|
return {
|
|
44749
45136
|
selected: false,
|
|
44750
45137
|
ok: true,
|
|
44751
|
-
mode
|
|
45138
|
+
mode,
|
|
44752
45139
|
api_url_configured: false,
|
|
44753
45140
|
api_key_configured: false,
|
|
44754
45141
|
v1_base_url: null,
|
|
@@ -44781,13 +45168,27 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env) {
|
|
|
44781
45168
|
local_fallback: false
|
|
44782
45169
|
};
|
|
44783
45170
|
}
|
|
45171
|
+
function serverStorageMode(normalize = normalizeStorageMode) {
|
|
45172
|
+
const useCache = normalize === normalizeStorageMode;
|
|
45173
|
+
if (useCache && cachedServerMode !== null)
|
|
45174
|
+
return cachedServerMode;
|
|
45175
|
+
for (const candidate of SERVER_MODE_CANDIDATES) {
|
|
45176
|
+
try {
|
|
45177
|
+
normalize(candidate);
|
|
45178
|
+
if (useCache)
|
|
45179
|
+
cachedServerMode = candidate;
|
|
45180
|
+
return candidate;
|
|
45181
|
+
} catch {}
|
|
45182
|
+
}
|
|
45183
|
+
throw new Error(`REMOTE_STORAGE_MODE_UNSUPPORTED: no known server storage mode is accepted by the installed ` + `@hasna/contracts (tried ${SERVER_MODE_CANDIDATES.join(", ")}); the storage-mode enum has changed. ` + `Add the new server token to SERVER_MODE_CANDIDATES in src/cli/cloud-router.ts; ` + `local SQLite fallback is disabled.`);
|
|
45184
|
+
}
|
|
44784
45185
|
function requireTodosRemoteAuthorityEnv(env) {
|
|
44785
45186
|
const status = getTodosRemoteAuthorityConfigStatus(env);
|
|
44786
45187
|
if (!status.ok)
|
|
44787
45188
|
throw new Error(status.issues[0]);
|
|
44788
45189
|
return {
|
|
44789
45190
|
...env,
|
|
44790
|
-
HASNA_TODOS_STORAGE_MODE:
|
|
45191
|
+
HASNA_TODOS_STORAGE_MODE: serverStorageMode(),
|
|
44791
45192
|
HASNA_TODOS_API_URL: status.v1_base_url.replace(/\/v1$/, ""),
|
|
44792
45193
|
HASNA_TODOS_API_KEY: env.HASNA_TODOS_API_KEY.trim()
|
|
44793
45194
|
};
|
|
@@ -44860,8 +45261,7 @@ async function requiredRemoteRoute(client, route, request) {
|
|
|
44860
45261
|
}
|
|
44861
45262
|
}
|
|
44862
45263
|
function getTodosCloudClient(env = process.env) {
|
|
44863
|
-
|
|
44864
|
-
if (!CLOUD_MODES.has(mode))
|
|
45264
|
+
if (requestedTransport(env) !== "http")
|
|
44865
45265
|
return null;
|
|
44866
45266
|
const resolved = resolveStorageClient("todos", requireTodosRemoteAuthorityEnv(env), {
|
|
44867
45267
|
fetchImpl: (input, init) => globalThis.fetch(input, { ...init, redirect: "manual" })
|
|
@@ -44896,13 +45296,38 @@ function toListQuery(filter = {}) {
|
|
|
44896
45296
|
query["assigned_to"] = filter.assigned_to;
|
|
44897
45297
|
if (filter.agent_id)
|
|
44898
45298
|
query["agent_id"] = filter.agent_id;
|
|
45299
|
+
if (filter.tags?.length)
|
|
45300
|
+
query["tags"] = filter.tags.join(",");
|
|
44899
45301
|
if (typeof filter.limit === "number")
|
|
44900
45302
|
query["limit"] = filter.limit;
|
|
44901
45303
|
if (typeof filter.offset === "number")
|
|
44902
45304
|
query["offset"] = filter.offset;
|
|
44903
45305
|
return query;
|
|
44904
45306
|
}
|
|
45307
|
+
async function fetchListTagsCapability(client) {
|
|
45308
|
+
const document = await requiredRemoteRoute(client, "/v1/openapi.json", () => client.transport.get("/openapi.json"));
|
|
45309
|
+
if (!document || typeof document !== "object" || Array.isArray(document))
|
|
45310
|
+
return false;
|
|
45311
|
+
const paths = document["paths"];
|
|
45312
|
+
const tasksPath = paths && typeof paths === "object" && !Array.isArray(paths) ? paths["/v1/tasks"] : undefined;
|
|
45313
|
+
const get = tasksPath && typeof tasksPath === "object" && !Array.isArray(tasksPath) ? tasksPath["get"] : undefined;
|
|
45314
|
+
const parameters = get && typeof get === "object" && !Array.isArray(get) ? get["parameters"] : undefined;
|
|
45315
|
+
return Array.isArray(parameters) && parameters.some((parameter) => parameter && typeof parameter === "object" && parameter["name"] === "tags");
|
|
45316
|
+
}
|
|
45317
|
+
async function requireTagsFilterCapability(client) {
|
|
45318
|
+
const authority = remoteAuthorityBase(client);
|
|
45319
|
+
let capability = listTagsCapabilityCache.get(authority);
|
|
45320
|
+
if (!capability) {
|
|
45321
|
+
capability = fetchListTagsCapability(client);
|
|
45322
|
+
listTagsCapabilityCache.set(authority, capability);
|
|
45323
|
+
}
|
|
45324
|
+
if (!await capability) {
|
|
45325
|
+
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");
|
|
45326
|
+
}
|
|
45327
|
+
}
|
|
44905
45328
|
async function cloudListTasks(client, filter = {}) {
|
|
45329
|
+
if (filter.tags?.length)
|
|
45330
|
+
await requireTagsFilterCapability(client);
|
|
44906
45331
|
const res = await requiredRemoteRoute(client, "/v1/tasks", () => client.list("tasks", { query: toListQuery(filter) }));
|
|
44907
45332
|
const envelope = res.raw;
|
|
44908
45333
|
return Array.isArray(envelope?.tasks) ? envelope.tasks : res.items;
|
|
@@ -45010,6 +45435,8 @@ function redactComment3(comment) {
|
|
|
45010
45435
|
}
|
|
45011
45436
|
async function cloudCountTasks(client, filter = {}) {
|
|
45012
45437
|
const { limit: _drop, offset: _o, ...rest } = filter;
|
|
45438
|
+
if (rest.tags?.length)
|
|
45439
|
+
await requireTagsFilterCapability(client);
|
|
45013
45440
|
const res = await requiredRemoteRoute(client, "/v1/tasks", () => client.list("tasks", { query: { ...toListQuery(rest), limit: 1 } }));
|
|
45014
45441
|
const envelope = res.raw;
|
|
45015
45442
|
if (typeof envelope?.total === "number")
|
|
@@ -45072,15 +45499,24 @@ async function cloudResolveTaskListRef(client, ref, projectId) {
|
|
|
45072
45499
|
}
|
|
45073
45500
|
throw new Error(`Task list not found: "${input}"`);
|
|
45074
45501
|
}
|
|
45075
|
-
var UUID_RE,
|
|
45502
|
+
var UUID_RE, TRANSPORT_TOKENS, completionCapabilityCache, SERVER_MODE_CANDIDATES, cachedServerMode = null, listTagsCapabilityCache;
|
|
45076
45503
|
var init_cloud_router = __esm(() => {
|
|
45077
45504
|
init_types();
|
|
45078
45505
|
init_redaction();
|
|
45079
45506
|
init_http_client();
|
|
45080
45507
|
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
|
-
|
|
45508
|
+
TRANSPORT_TOKENS = {
|
|
45509
|
+
sqlite: "sqlite",
|
|
45510
|
+
http: "http",
|
|
45511
|
+
local: "sqlite",
|
|
45512
|
+
remote: "http",
|
|
45513
|
+
self_hosted: "http",
|
|
45514
|
+
cloud: "http",
|
|
45515
|
+
hybrid: "http"
|
|
45516
|
+
};
|
|
45083
45517
|
completionCapabilityCache = new Map;
|
|
45518
|
+
SERVER_MODE_CANDIDATES = ["postgres", "cloud", "self_hosted"];
|
|
45519
|
+
listTagsCapabilityCache = new Map;
|
|
45084
45520
|
});
|
|
45085
45521
|
|
|
45086
45522
|
// src/mcp/tools/task-crud.ts
|
|
@@ -48666,12 +49102,11 @@ ${task.description || ""}`.split(/\r?\n/);
|
|
|
48666
49102
|
function normalizeStackLocation(value) {
|
|
48667
49103
|
return value.replace(/:\d+(?::\d+)?/g, "").replace(/^file:\/\//, "").toLowerCase().trim();
|
|
48668
49104
|
}
|
|
48669
|
-
function
|
|
49105
|
+
function fingerprintContent(task) {
|
|
48670
49106
|
const body = task.description || "";
|
|
48671
49107
|
const text = `${task.title}
|
|
48672
49108
|
${body}`;
|
|
48673
49109
|
return {
|
|
48674
|
-
task,
|
|
48675
49110
|
title: normalizeText(task.title),
|
|
48676
49111
|
body: normalizeText(body),
|
|
48677
49112
|
text: normalizeText(text),
|
|
@@ -48680,6 +49115,9 @@ ${body}`;
|
|
|
48680
49115
|
stackKeys: stackKeysFor(task)
|
|
48681
49116
|
};
|
|
48682
49117
|
}
|
|
49118
|
+
function fingerprint(task) {
|
|
49119
|
+
return { task, ...fingerprintContent(task) };
|
|
49120
|
+
}
|
|
48683
49121
|
function intersects(left, right) {
|
|
48684
49122
|
for (const value of left)
|
|
48685
49123
|
if (right.has(value))
|
|
@@ -49888,7 +50326,7 @@ function tableColumns(db, table) {
|
|
|
49888
50326
|
}
|
|
49889
50327
|
function assertMigratedSchema(db) {
|
|
49890
50328
|
const issues = [];
|
|
49891
|
-
for (const table of
|
|
50329
|
+
for (const table of REQUIRED_TABLES3) {
|
|
49892
50330
|
if (!hasTable(db, table))
|
|
49893
50331
|
issues.push(`missing table ${table}`);
|
|
49894
50332
|
}
|
|
@@ -49996,7 +50434,7 @@ function createReleaseCompatibilityReport(options = {}) {
|
|
|
49996
50434
|
migrations: {
|
|
49997
50435
|
current_level: MIGRATIONS.length,
|
|
49998
50436
|
simulated_levels: simulatedLevels,
|
|
49999
|
-
checked_tables:
|
|
50437
|
+
checked_tables: REQUIRED_TABLES3,
|
|
50000
50438
|
checked_columns: REQUIRED_COLUMNS
|
|
50001
50439
|
},
|
|
50002
50440
|
exports: {
|
|
@@ -50016,7 +50454,7 @@ function createReleaseCompatibilityReport(options = {}) {
|
|
|
50016
50454
|
"todos --help",
|
|
50017
50455
|
"todos-mcp --help",
|
|
50018
50456
|
"todos-serve --help",
|
|
50019
|
-
"todos doctor"
|
|
50457
|
+
"todos doctor --no-fail-on-findings"
|
|
50020
50458
|
],
|
|
50021
50459
|
smoke_tests: [
|
|
50022
50460
|
"command -v todos",
|
|
@@ -50024,7 +50462,7 @@ function createReleaseCompatibilityReport(options = {}) {
|
|
|
50024
50462
|
"command -v todos-serve",
|
|
50025
50463
|
"todos --version",
|
|
50026
50464
|
"todos --help",
|
|
50027
|
-
"todos doctor"
|
|
50465
|
+
"todos doctor --no-fail-on-findings"
|
|
50028
50466
|
],
|
|
50029
50467
|
rollback: [
|
|
50030
50468
|
"npm view @hasna/todos versions --json",
|
|
@@ -50073,14 +50511,14 @@ function renderReleaseCompatibilityMarkdown(report) {
|
|
|
50073
50511
|
return lines.join(`
|
|
50074
50512
|
`);
|
|
50075
50513
|
}
|
|
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,
|
|
50514
|
+
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
50515
|
var init_release_compatibility = __esm(() => {
|
|
50078
50516
|
init_migrations();
|
|
50079
50517
|
init_schema();
|
|
50080
50518
|
EXPECTED_BINS = ["todos", "todos-mcp", "todos-serve"];
|
|
50081
50519
|
EXPECTED_EXPORTS = [".", "./sdk", "./mcp", "./registry", "./contracts", "./storage"];
|
|
50082
50520
|
REQUIRED_SCRIPTS = ["build", "test:no-cloud", "verify:release", "prepublishOnly"];
|
|
50083
|
-
|
|
50521
|
+
REQUIRED_TABLES3 = [
|
|
50084
50522
|
"_migrations",
|
|
50085
50523
|
"projects",
|
|
50086
50524
|
"tasks",
|
|
@@ -53839,7 +54277,7 @@ function limits(input) {
|
|
|
53839
54277
|
verification_limit: clamp(input.verification_limit, DEFAULT_LIMITS.verification_limit, 100),
|
|
53840
54278
|
run_limit: clamp(input.run_limit, DEFAULT_LIMITS.run_limit, 20),
|
|
53841
54279
|
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,
|
|
54280
|
+
plan_task_limit: clamp(input.plan_task_limit, DEFAULT_LIMITS.plan_task_limit, MAX_CONTEXT_TASKS),
|
|
53843
54281
|
max_text_chars: clamp(input.max_text_chars, DEFAULT_LIMITS.max_text_chars, 50000),
|
|
53844
54282
|
summary_char_limit: clamp(input.summary_char_limit, DEFAULT_LIMITS.summary_char_limit, 4000),
|
|
53845
54283
|
stale_after_hours: clamp(input.stale_after_hours, DEFAULT_LIMITS.stale_after_hours, 24 * 365)
|
|
@@ -53862,6 +54300,13 @@ function taskSummary(task) {
|
|
|
53862
54300
|
priority: task.priority
|
|
53863
54301
|
};
|
|
53864
54302
|
}
|
|
54303
|
+
function latestPlanTasks(planId, limit, db) {
|
|
54304
|
+
const rows = db.query(`SELECT * FROM tasks
|
|
54305
|
+
WHERE plan_id = ? AND archived_at IS NULL
|
|
54306
|
+
ORDER BY updated_at DESC, created_at DESC, id ASC
|
|
54307
|
+
LIMIT ?`).all(planId, limit);
|
|
54308
|
+
return rows.map(rowToTask);
|
|
54309
|
+
}
|
|
53865
54310
|
function acceptanceCriteria(task, maxText) {
|
|
53866
54311
|
const metadata = task.metadata || {};
|
|
53867
54312
|
const raw = metadata["acceptance_criteria"] ?? metadata["acceptanceCriteria"] ?? metadata["criteria"];
|
|
@@ -53959,7 +54404,7 @@ function summarizeSection(pack, section, maxChars) {
|
|
|
53959
54404
|
return "No plan was attached.";
|
|
53960
54405
|
return summarizeStrings([
|
|
53961
54406
|
`Plan ${pack.plan.name} is ${pack.plan.status}`,
|
|
53962
|
-
`${pack.plan.tasks.length} listed plan
|
|
54407
|
+
`${pack.plan.tasks.length} of ${pack.plan.total_tasks} plan tasks listed; ${pack.plan.omitted_tasks} omitted`,
|
|
53963
54408
|
...pack.plan.tasks.slice(0, 4).map((task) => `${task.status} ${task.short_id || task.id.slice(0, 8)} ${task.title}`)
|
|
53964
54409
|
], maxChars);
|
|
53965
54410
|
}
|
|
@@ -54109,7 +54554,8 @@ function createAgentContextPack(input, db) {
|
|
|
54109
54554
|
const limit = limits(input);
|
|
54110
54555
|
const project = task.project_id ? getProject(task.project_id, d) : null;
|
|
54111
54556
|
const plan = task.plan_id ? getPlan(task.plan_id, d) : null;
|
|
54112
|
-
const planTasks = task.plan_id ?
|
|
54557
|
+
const planTasks = task.plan_id ? latestPlanTasks(task.plan_id, limit.plan_task_limit, d) : [];
|
|
54558
|
+
const totalPlanTasks = task.plan_id ? countTasks({ plan_id: task.plan_id }, d) : 0;
|
|
54113
54559
|
const upstream = getTaskDependencies(task.id, d).map((dep) => taskSummary(getTask(dep.depends_on, d))).filter((item) => Boolean(item));
|
|
54114
54560
|
const downstream = getTaskDependents(task.id, d).map((dep) => taskSummary(getTask(dep.task_id, d))).filter((item) => Boolean(item));
|
|
54115
54561
|
const comments = listComments(task.id, d);
|
|
@@ -54152,6 +54598,9 @@ function createAgentContextPack(input, db) {
|
|
|
54152
54598
|
warnings.push(`${runs.length - selectedRuns.length} older runs omitted`);
|
|
54153
54599
|
if (fileMap.size > relevantFiles.length)
|
|
54154
54600
|
warnings.push(`${fileMap.size - relevantFiles.length} relevant files omitted`);
|
|
54601
|
+
if (totalPlanTasks > planTasks.length && plan) {
|
|
54602
|
+
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>"`);
|
|
54603
|
+
}
|
|
54155
54604
|
const contextTask = {
|
|
54156
54605
|
id: task.id,
|
|
54157
54606
|
short_id: task.short_id,
|
|
@@ -54183,8 +54632,9 @@ function createAgentContextPack(input, db) {
|
|
|
54183
54632
|
description: truncate2(plan.description, limit.max_text_chars),
|
|
54184
54633
|
status: plan.status,
|
|
54185
54634
|
agent_id: plan.agent_id,
|
|
54186
|
-
tasks: planTasks.
|
|
54187
|
-
|
|
54635
|
+
tasks: planTasks.map(taskSummary).filter((item) => Boolean(item)),
|
|
54636
|
+
total_tasks: totalPlanTasks,
|
|
54637
|
+
omitted_tasks: Math.max(0, totalPlanTasks - planTasks.length)
|
|
54188
54638
|
} : null,
|
|
54189
54639
|
acceptance_criteria: acceptanceCriteria(task, limit.max_text_chars),
|
|
54190
54640
|
dependencies: {
|
|
@@ -54294,6 +54744,8 @@ ${pack.task.description}` : null,
|
|
|
54294
54744
|
"## Project And Plan",
|
|
54295
54745
|
pack.project ? `- Project: ${pack.project.name} (${pack.project.path})` : "- Project: none",
|
|
54296
54746
|
pack.plan ? `- Plan: ${pack.plan.name} (${pack.plan.status})` : "- Plan: none",
|
|
54747
|
+
pack.plan ? `- Plan tasks: ${pack.plan.tasks.length} shown of ${pack.plan.total_tasks} total (${pack.plan.omitted_tasks} omitted)` : null,
|
|
54748
|
+
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
54749
|
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
54750
|
"",
|
|
54299
54751
|
"## Acceptance Criteria",
|
|
@@ -54374,7 +54826,7 @@ function renderAgentContextPack(pack, format, compact2 = false) {
|
|
|
54374
54826
|
return compact2 ? renderAgentContextPackCompactMarkdown(pack) : renderAgentContextPackMarkdown(pack);
|
|
54375
54827
|
return compact2 ? JSON.stringify(pack) : JSON.stringify(pack, null, 2);
|
|
54376
54828
|
}
|
|
54377
|
-
var DEFAULT_LIMITS, ALL_CONTEXT_SECTIONS, BUDGET_TRIM_ORDER;
|
|
54829
|
+
var DEFAULT_LIMITS, MAX_CONTEXT_TASKS = 24, ALL_CONTEXT_SECTIONS, BUDGET_TRIM_ORDER;
|
|
54378
54830
|
var init_context_packs = __esm(() => {
|
|
54379
54831
|
init_comments();
|
|
54380
54832
|
init_database();
|
|
@@ -54393,7 +54845,7 @@ var init_context_packs = __esm(() => {
|
|
|
54393
54845
|
verification_limit: 10,
|
|
54394
54846
|
run_limit: 3,
|
|
54395
54847
|
dependency_limit: 12,
|
|
54396
|
-
plan_task_limit:
|
|
54848
|
+
plan_task_limit: 24,
|
|
54397
54849
|
max_text_chars: 6000,
|
|
54398
54850
|
summary_char_limit: 480,
|
|
54399
54851
|
stale_after_hours: 72
|
|
@@ -79216,7 +79668,7 @@ function parseObject2(value) {
|
|
|
79216
79668
|
function normalizeKey(value) {
|
|
79217
79669
|
return value.trim().toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
79218
79670
|
}
|
|
79219
|
-
function
|
|
79671
|
+
function normalizeTaskFindingFingerprint(value) {
|
|
79220
79672
|
const normalized = normalizeKey(value);
|
|
79221
79673
|
if (!normalized)
|
|
79222
79674
|
throw new Error("finding fingerprint is required");
|
|
@@ -79325,7 +79777,7 @@ function assertTask(taskId, db) {
|
|
|
79325
79777
|
throw new TaskNotFoundError(taskId);
|
|
79326
79778
|
}
|
|
79327
79779
|
function nextFinding(input, db) {
|
|
79328
|
-
const fingerprint2 =
|
|
79780
|
+
const fingerprint2 = normalizeTaskFindingFingerprint(input.fingerprint);
|
|
79329
79781
|
const title = redactOptional(input.title, 300);
|
|
79330
79782
|
if (!title)
|
|
79331
79783
|
throw new Error("finding title is required");
|
|
@@ -79473,7 +79925,7 @@ function resolveMissingTaskFindings(input, db) {
|
|
|
79473
79925
|
const timestamp4 = input.resolved_at || now();
|
|
79474
79926
|
const status = normalizeResolutionStatus(input.status);
|
|
79475
79927
|
const runId = resolveRunForTask(input.run_id, input.task_id, d);
|
|
79476
|
-
const present = new Set(input.fingerprints.map(
|
|
79928
|
+
const present = new Set(input.fingerprints.map(normalizeTaskFindingFingerprint));
|
|
79477
79929
|
const warnings = [];
|
|
79478
79930
|
const conditions = ["task_id = ?", "status = 'open'"];
|
|
79479
79931
|
const params = [input.task_id];
|
|
@@ -91218,6 +91670,15 @@ function formatError2(error3) {
|
|
|
91218
91670
|
if (error3 instanceof TaskNotFoundError) {
|
|
91219
91671
|
return JSON.stringify({ code: TaskNotFoundError.code, message: error3.message, suggestion: TaskNotFoundError.suggestion });
|
|
91220
91672
|
}
|
|
91673
|
+
if (error3 instanceof TaskReferenceAmbiguousError) {
|
|
91674
|
+
return JSON.stringify({
|
|
91675
|
+
code: TaskReferenceAmbiguousError.code,
|
|
91676
|
+
message: error3.message,
|
|
91677
|
+
candidate_project_ids: error3.candidateProjectIds,
|
|
91678
|
+
candidate_task_ids: error3.candidateTaskIds,
|
|
91679
|
+
suggestion: "Use a full task UUID."
|
|
91680
|
+
});
|
|
91681
|
+
}
|
|
91221
91682
|
if (error3 instanceof ProjectNotFoundError) {
|
|
91222
91683
|
return JSON.stringify({ code: ProjectNotFoundError.code, message: error3.message, suggestion: ProjectNotFoundError.suggestion });
|
|
91223
91684
|
}
|
|
@@ -92015,23 +92476,24 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
92015
92476
|
}
|
|
92016
92477
|
if ((path === "/health" || path === "/ready" || path === "/version") && method === "GET") {
|
|
92017
92478
|
const { getPackageVersion: getPackageVersion2 } = await Promise.resolve().then(() => (init_package_version(), exports_package_version));
|
|
92018
|
-
const {
|
|
92019
|
-
const
|
|
92479
|
+
const { isPostgresBackendConfigured: isPostgresBackendConfigured2, pingCloud: pingCloud2 } = await Promise.resolve().then(() => (init_cloud(), exports_cloud));
|
|
92480
|
+
const backend = isPostgresBackendConfigured2() ? "postgresql" : "sqlite";
|
|
92481
|
+
const mode = backend === "postgresql" ? "remote" : "local";
|
|
92020
92482
|
const version2 = getPackageVersion2();
|
|
92021
92483
|
if (path === "/version") {
|
|
92022
|
-
return Response.json({ status: "ok", version: version2, mode, name: "todos" });
|
|
92484
|
+
return Response.json({ status: "ok", version: version2, mode, backend, name: "todos" });
|
|
92023
92485
|
}
|
|
92024
92486
|
if (path === "/ready") {
|
|
92025
|
-
if (
|
|
92487
|
+
if (backend === "postgresql") {
|
|
92026
92488
|
try {
|
|
92027
92489
|
await pingCloud2();
|
|
92028
92490
|
} catch (e) {
|
|
92029
|
-
return Response.json({ status: "unavailable", version: version2, mode, error: e.message }, { status: 503 });
|
|
92491
|
+
return Response.json({ status: "unavailable", version: version2, mode, backend, error: e.message }, { status: 503 });
|
|
92030
92492
|
}
|
|
92031
92493
|
}
|
|
92032
|
-
return Response.json({ status: "ready", version: version2, mode });
|
|
92494
|
+
return Response.json({ status: "ready", version: version2, mode, backend });
|
|
92033
92495
|
}
|
|
92034
|
-
return Response.json({ status: "ok", version: version2, mode, name: "todos" });
|
|
92496
|
+
return Response.json({ status: "ok", version: version2, mode, backend, name: "todos" });
|
|
92035
92497
|
}
|
|
92036
92498
|
if ((path === "/openapi.json" || path === "/v1/openapi.json") && method === "GET") {
|
|
92037
92499
|
const { buildV1OpenApiDocument: buildV1OpenApiDocument2 } = await Promise.resolve().then(() => (init_openapi(), exports_openapi));
|