@hasna/todos 0.13.1 → 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 +80 -15
- package/dist/cli/cloud-router.d.ts +78 -12
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/config-serve-commands.d.ts.map +1 -1
- package/dist/cli/commands/query-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-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 +1748 -1012
- package/dist/cli/stage-a.d.ts.map +1 -1
- package/dist/contracts.js +137 -39
- 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/db/task-crud.d.ts.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +592 -147
- 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 +19 -11
- package/dist/lib/dependency-graph.d.ts.map +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/redaction.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 +544 -97
- package/dist/mcp.js +8 -4
- package/dist/registry.d.ts +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +144 -39
- 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 +585 -118
- package/dist/server/openapi.d.ts +12 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/port.d.ts +52 -0
- package/dist/server/port.d.ts.map +1 -0
- 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 +43 -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 +619 -256
- 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 +27 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +8 -4
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,13 +120,13 @@ 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",
|
|
124
128
|
"build:dashboard": "cd dashboard && bun install --frozen-lockfile && bun run build",
|
|
125
|
-
typecheck: "tsc --noEmit",
|
|
129
|
+
typecheck: "tsc --noEmit -p tsconfig.typecheck.json",
|
|
126
130
|
test: "bun test",
|
|
127
131
|
"test:no-cloud": "bun test src/no-cloud-boundary.test.ts src/local-first.test.ts src/lib/public-release-gate.test.ts",
|
|
128
132
|
"dev:cli": "bun run src/cli/index.tsx",
|
|
@@ -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
|
|
@@ -466,7 +493,10 @@ var init_cloud_client = __esm(() => {
|
|
|
466
493
|
});
|
|
467
494
|
|
|
468
495
|
// src/types/index.ts
|
|
469
|
-
|
|
496
|
+
function isBlockingDependencyStatus(status) {
|
|
497
|
+
return status !== "completed" && status !== "cancelled";
|
|
498
|
+
}
|
|
499
|
+
var TASK_STATUSES, VersionConflictError, TaskNotFoundError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
|
|
470
500
|
var init_types = __esm(() => {
|
|
471
501
|
TASK_STATUSES = [
|
|
472
502
|
"pending",
|
|
@@ -499,6 +529,22 @@ var init_types = __esm(() => {
|
|
|
499
529
|
this.name = "TaskNotFoundError";
|
|
500
530
|
}
|
|
501
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
|
+
};
|
|
502
548
|
ProjectNotFoundError = class ProjectNotFoundError extends Error {
|
|
503
549
|
projectId;
|
|
504
550
|
static code = "PROJECT_NOT_FOUND";
|
|
@@ -1083,6 +1129,185 @@ var init_postgres_sync = __esm(() => {
|
|
|
1083
1129
|
};
|
|
1084
1130
|
});
|
|
1085
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
|
+
|
|
1086
1311
|
// src/lib/sync-utils.ts
|
|
1087
1312
|
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
|
|
1088
1313
|
import { join } from "path";
|
|
@@ -1201,11 +1426,15 @@ function secretPatterns() {
|
|
|
1201
1426
|
return [...customPatterns(), ...DEFAULT_SECRET_PATTERNS];
|
|
1202
1427
|
}
|
|
1203
1428
|
function isRedactionPlaceholderMatch(match) {
|
|
1204
|
-
const placeholder = String.raw`\[REDACTED(?:_[A-Z_]+)?\]`;
|
|
1205
1429
|
const trimmed = match.trim();
|
|
1206
|
-
return new RegExp(`^${
|
|
1430
|
+
return new RegExp(`^${REDACTION_PLACEHOLDER}$`).test(trimmed) || new RegExp(`=\\s*['"]?${REDACTION_PLACEHOLDER}['"]?$`).test(trimmed);
|
|
1431
|
+
}
|
|
1432
|
+
function isRedactionPlaceholderKey(key) {
|
|
1433
|
+
return new RegExp(`^${REDACTION_PLACEHOLDER}$`).test(key.trim());
|
|
1207
1434
|
}
|
|
1208
1435
|
function isSecretKey(key) {
|
|
1436
|
+
if (isRedactionPlaceholderKey(key))
|
|
1437
|
+
return false;
|
|
1209
1438
|
if (NON_SECRET_USAGE_KEYS.has(key.toLowerCase()))
|
|
1210
1439
|
return false;
|
|
1211
1440
|
if (DEFAULT_SECRET_KEY_PATTERN.test(key))
|
|
@@ -1263,7 +1492,7 @@ function upsertSecretSafetyConfig(input) {
|
|
|
1263
1492
|
saveConfig({ ...config, secret_safety: next });
|
|
1264
1493
|
return next;
|
|
1265
1494
|
}
|
|
1266
|
-
var DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS;
|
|
1495
|
+
var DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS, REDACTION_PLACEHOLDER;
|
|
1267
1496
|
var init_redaction = __esm(() => {
|
|
1268
1497
|
init_config2();
|
|
1269
1498
|
DEFAULT_SECRET_PATTERNS = [
|
|
@@ -1287,6 +1516,7 @@ var init_redaction = __esm(() => {
|
|
|
1287
1516
|
"completion_tokens",
|
|
1288
1517
|
"cost_tokens"
|
|
1289
1518
|
]);
|
|
1519
|
+
REDACTION_PLACEHOLDER = String.raw`\[REDACTED(?:_[A-Z_]+)?\]`;
|
|
1290
1520
|
});
|
|
1291
1521
|
|
|
1292
1522
|
// src/storage/postgres-adapter.ts
|
|
@@ -1417,6 +1647,9 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
1417
1647
|
exportSnapshot: () => exportSnapshot(store),
|
|
1418
1648
|
importSnapshot: (snapshot, context) => importSnapshot(snapshot, store, context)
|
|
1419
1649
|
},
|
|
1650
|
+
integrity: {
|
|
1651
|
+
report: () => store.integrityReport()
|
|
1652
|
+
},
|
|
1420
1653
|
transaction: (fn) => fn(adapter)
|
|
1421
1654
|
};
|
|
1422
1655
|
return adapter;
|
|
@@ -1523,8 +1756,9 @@ class PostgresJsonRecordStore {
|
|
|
1523
1756
|
conds.push(`payload->>'agent_id' = ${p(filter.agent_id)}`);
|
|
1524
1757
|
if (filter.session_id !== undefined)
|
|
1525
1758
|
conds.push(`payload->>'session_id' = ${p(filter.session_id)}`);
|
|
1526
|
-
if (filter.tags?.length)
|
|
1527
|
-
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
|
+
}
|
|
1528
1762
|
if (filter.has_recurrence !== undefined) {
|
|
1529
1763
|
conds.push(`(COALESCE(payload->>'recurrence_rule', '') <> '') = ${p(filter.has_recurrence)}`);
|
|
1530
1764
|
}
|
|
@@ -1585,7 +1819,8 @@ class PostgresJsonRecordStore {
|
|
|
1585
1819
|
AND object_id COLLATE "C" >= $2 AND object_id COLLATE "C" < $3
|
|
1586
1820
|
LIMIT 2`, [this.service, raw, upper]);
|
|
1587
1821
|
if (prefixResult.rows.length > 1) {
|
|
1588
|
-
|
|
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 })));
|
|
1589
1824
|
}
|
|
1590
1825
|
if (prefixResult.rows.length === 1) {
|
|
1591
1826
|
return payloadRecord2(prefixResult.rows[0].payload);
|
|
@@ -1596,7 +1831,8 @@ class PostgresJsonRecordStore {
|
|
|
1596
1831
|
AND LOWER(payload->>'short_id') = $2
|
|
1597
1832
|
LIMIT 2`, [this.service, raw]);
|
|
1598
1833
|
if (shortIdResult.rows.length > 1) {
|
|
1599
|
-
|
|
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 })));
|
|
1600
1836
|
}
|
|
1601
1837
|
if (shortIdResult.rows.length === 1) {
|
|
1602
1838
|
return payloadRecord2(shortIdResult.rows[0].payload);
|
|
@@ -1610,6 +1846,24 @@ class PostgresJsonRecordStore {
|
|
|
1610
1846
|
const result = await this.options.client.query(sql, params);
|
|
1611
1847
|
return Number(result.rows[0]?.count ?? 0);
|
|
1612
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
|
+
}
|
|
1613
1867
|
async listTombstones() {
|
|
1614
1868
|
await this.ensureSchema();
|
|
1615
1869
|
const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
|
|
@@ -2085,7 +2339,7 @@ async function startTask(id, agentId, store) {
|
|
|
2085
2339
|
if (task.status !== "pending" && task.status !== "in_progress") {
|
|
2086
2340
|
throw new Error(`Task is ${task.status} and cannot be started by ${agentId}`);
|
|
2087
2341
|
}
|
|
2088
|
-
|
|
2342
|
+
const started = await patchTask(task, {
|
|
2089
2343
|
status: "in_progress",
|
|
2090
2344
|
assigned_to: task.assigned_to ?? agentId,
|
|
2091
2345
|
agent_id: task.agent_id ?? agentId,
|
|
@@ -2093,6 +2347,8 @@ async function startTask(id, agentId, store) {
|
|
|
2093
2347
|
locked_at: new Date().toISOString(),
|
|
2094
2348
|
started_at: task.started_at ?? new Date().toISOString()
|
|
2095
2349
|
}, store);
|
|
2350
|
+
await logTaskChange(task.id, "start", "status", task.status, "in_progress", agentId, store);
|
|
2351
|
+
return started;
|
|
2096
2352
|
}
|
|
2097
2353
|
async function completeTask(id, agentId, options, store) {
|
|
2098
2354
|
const task = await store.completeTask(id, agentId, options);
|
|
@@ -2209,9 +2465,11 @@ async function removeDependency(taskId, dependsOn, store) {
|
|
|
2209
2465
|
}
|
|
2210
2466
|
async function listDependencies(taskId, store) {
|
|
2211
2467
|
const edges = await store.list("dependencies");
|
|
2468
|
+
const incoming = edges.filter((edge) => edge.depends_on === taskId).map((edge) => ({ task_id: edge.task_id, depends_on: edge.depends_on }));
|
|
2212
2469
|
return {
|
|
2213
2470
|
dependencies: edges.filter((edge) => edge.task_id === taskId).map((edge) => ({ task_id: edge.task_id, depends_on: edge.depends_on })),
|
|
2214
|
-
|
|
2471
|
+
blocks: incoming,
|
|
2472
|
+
blocked_by: incoming
|
|
2215
2473
|
};
|
|
2216
2474
|
}
|
|
2217
2475
|
async function addVerification(input, store, context) {
|
|
@@ -2757,6 +3015,7 @@ var CLOUD_LOCK_EXPIRY_MINUTES = 30, TASK_ORDER_TIEBREAK = "CASE payload->>'prior
|
|
|
2757
3015
|
var init_postgres_adapter = __esm(() => {
|
|
2758
3016
|
init_types();
|
|
2759
3017
|
init_postgres_sync();
|
|
3018
|
+
init_integrity();
|
|
2760
3019
|
init_redaction();
|
|
2761
3020
|
TASK_ORDER_BY = `ORDER BY ${TASK_ORDER_TIEBREAK}`;
|
|
2762
3021
|
});
|
|
@@ -4755,6 +5014,7 @@ __export(exports_cloud, {
|
|
|
4755
5014
|
resolveCloudDatabaseUrl: () => resolveCloudDatabaseUrl,
|
|
4756
5015
|
pingCloud: () => pingCloud,
|
|
4757
5016
|
normalizeCloudPayloads: () => normalizeCloudPayloads,
|
|
5017
|
+
isPostgresBackendConfigured: () => isPostgresBackendConfigured,
|
|
4758
5018
|
isCloudModeEnabled: () => isCloudModeEnabled,
|
|
4759
5019
|
getCloudVerifier: () => getCloudVerifier,
|
|
4760
5020
|
getCloudStorageAdapter: () => getCloudStorageAdapter,
|
|
@@ -4777,9 +5037,12 @@ function resolveCloudDatabaseUrl(env = process.env) {
|
|
|
4777
5037
|
function resolveSigningSecret(env = process.env) {
|
|
4778
5038
|
return env.HASNA_TODOS_API_SIGNING_KEY || env.HASNA_API_SIGNING_KEY || env.API_KEY_SIGNING_SECRET || undefined;
|
|
4779
5039
|
}
|
|
4780
|
-
function
|
|
5040
|
+
function isPostgresBackendConfigured(env = process.env) {
|
|
4781
5041
|
return Boolean(resolveCloudDatabaseUrl(env));
|
|
4782
5042
|
}
|
|
5043
|
+
function isCloudModeEnabled(env = process.env) {
|
|
5044
|
+
return isPostgresBackendConfigured(env);
|
|
5045
|
+
}
|
|
4783
5046
|
function getClient() {
|
|
4784
5047
|
if (cachedClient)
|
|
4785
5048
|
return cachedClient;
|
|
@@ -8607,6 +8870,30 @@ function findGitRoot(startDir) {
|
|
|
8607
8870
|
}
|
|
8608
8871
|
return null;
|
|
8609
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
|
+
}
|
|
8610
8897
|
function getDbPath() {
|
|
8611
8898
|
if (process.env["HASNA_TODOS_DB_PATH"]) {
|
|
8612
8899
|
return process.env["HASNA_TODOS_DB_PATH"];
|
|
@@ -8614,18 +8901,19 @@ function getDbPath() {
|
|
|
8614
8901
|
if (process.env["TODOS_DB_PATH"]) {
|
|
8615
8902
|
return process.env["TODOS_DB_PATH"];
|
|
8616
8903
|
}
|
|
8904
|
+
if (hasExplicitProjectArg())
|
|
8905
|
+
return getGlobalDbPath();
|
|
8617
8906
|
const cwd = process.cwd();
|
|
8618
8907
|
const nearest = findNearestProjectDb(cwd);
|
|
8619
8908
|
if (nearest)
|
|
8620
8909
|
return nearest;
|
|
8621
8910
|
if (process.env["TODOS_DB_SCOPE"] === "project") {
|
|
8622
8911
|
const gitRoot = findGitRoot(cwd);
|
|
8623
|
-
if (gitRoot) {
|
|
8912
|
+
if (gitRoot && canCreateScopedProjectDb()) {
|
|
8624
8913
|
return join3(gitRoot, ".hasna", "todos", "todos.db");
|
|
8625
8914
|
}
|
|
8626
8915
|
}
|
|
8627
|
-
|
|
8628
|
-
return join3(home, ".hasna", "todos", "todos.db");
|
|
8916
|
+
return getGlobalDbPath();
|
|
8629
8917
|
}
|
|
8630
8918
|
function getDatabasePath() {
|
|
8631
8919
|
return getDbPath();
|
|
@@ -8641,8 +8929,8 @@ function ensureDir2(filePath) {
|
|
|
8641
8929
|
function openDatabase(path) {
|
|
8642
8930
|
ensureDir2(path);
|
|
8643
8931
|
const db = new Database(path);
|
|
8644
|
-
db.run("PRAGMA journal_mode = WAL");
|
|
8645
8932
|
db.run("PRAGMA busy_timeout = 5000");
|
|
8933
|
+
db.run("PRAGMA journal_mode = WAL");
|
|
8646
8934
|
db.run("PRAGMA foreign_keys = ON");
|
|
8647
8935
|
runMigrations(db);
|
|
8648
8936
|
ensureAgentIdentitySchema(db);
|
|
@@ -8728,10 +9016,13 @@ function resolvePartialId(db, table, partialId) {
|
|
|
8728
9016
|
return null;
|
|
8729
9017
|
}
|
|
8730
9018
|
if (table === "tasks") {
|
|
8731
|
-
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);
|
|
8732
9020
|
if (shortIdRows.length === 1) {
|
|
8733
9021
|
return shortIdRows[0].id;
|
|
8734
9022
|
}
|
|
9023
|
+
if (shortIdRows.length > 1) {
|
|
9024
|
+
throw new TaskReferenceAmbiguousError(partialId, shortIdRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
|
|
9025
|
+
}
|
|
8735
9026
|
}
|
|
8736
9027
|
if (table === "task_lists") {
|
|
8737
9028
|
const slugRow = db.query("SELECT id FROM task_lists WHERE slug = ?").get(partialId);
|
|
@@ -8895,7 +9186,7 @@ function authNotConfiguredMessage(host) {
|
|
|
8895
9186
|
`);
|
|
8896
9187
|
}
|
|
8897
9188
|
function resolveAuthPosture(input) {
|
|
8898
|
-
const hosted = input.hosted ??
|
|
9189
|
+
const hosted = input.hosted ?? isPostgresBackendConfigured();
|
|
8899
9190
|
const hasCredentialSource = Boolean(input.apiKey) || input.hasGeneratedKeys;
|
|
8900
9191
|
if (hasCredentialSource) {
|
|
8901
9192
|
return {
|
|
@@ -11040,7 +11331,7 @@ function scanTextForSecrets(text, options = {}) {
|
|
|
11040
11331
|
};
|
|
11041
11332
|
}
|
|
11042
11333
|
function redactText(text, options = {}) {
|
|
11043
|
-
const placeholder = options.placeholder ??
|
|
11334
|
+
const placeholder = options.placeholder ?? REDACTION_PLACEHOLDER2;
|
|
11044
11335
|
let out = text;
|
|
11045
11336
|
for (const { pattern, allowlist_ok } of DEFAULT_PATTERNS) {
|
|
11046
11337
|
out = out.replace(new RegExp(pattern.source, pattern.flags), (match) => {
|
|
@@ -11067,7 +11358,7 @@ function redactExportRecord(record) {
|
|
|
11067
11358
|
}
|
|
11068
11359
|
return base;
|
|
11069
11360
|
}
|
|
11070
|
-
var SECRET_REDACTION_SCHEMA,
|
|
11361
|
+
var SECRET_REDACTION_SCHEMA, REDACTION_PLACEHOLDER2 = "[REDACTED]", DEFAULT_PATTERNS, DEFAULT_ALLOWLIST, customRedactors;
|
|
11071
11362
|
var init_secret_redaction = __esm(() => {
|
|
11072
11363
|
init_redaction();
|
|
11073
11364
|
SECRET_REDACTION_SCHEMA = ["todos", "secret_redaction", "v1"].join(".");
|
|
@@ -13059,10 +13350,11 @@ function getTaskWithRelations(id, db) {
|
|
|
13059
13350
|
JOIN task_dependencies td ON td.depends_on = t.id
|
|
13060
13351
|
WHERE td.task_id = ?`).all(id);
|
|
13061
13352
|
const dependencies = depRows.map(rowToTask);
|
|
13062
|
-
const
|
|
13353
|
+
const blocked_by = dependencies.filter((dep) => isBlockingDependencyStatus(dep.status));
|
|
13354
|
+
const blocksRows = d.query(`SELECT t.* FROM tasks t
|
|
13063
13355
|
JOIN task_dependencies td ON td.task_id = t.id
|
|
13064
13356
|
WHERE td.depends_on = ?`).all(id);
|
|
13065
|
-
const
|
|
13357
|
+
const blocks = blocksRows.map(rowToTask);
|
|
13066
13358
|
const comments = d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(id);
|
|
13067
13359
|
const parent = task.parent_id ? getTask(task.parent_id, d) : null;
|
|
13068
13360
|
const checklist = getChecklist(id, d);
|
|
@@ -13071,6 +13363,7 @@ function getTaskWithRelations(id, db) {
|
|
|
13071
13363
|
subtasks,
|
|
13072
13364
|
dependencies,
|
|
13073
13365
|
blocked_by,
|
|
13366
|
+
blocks,
|
|
13074
13367
|
comments,
|
|
13075
13368
|
parent,
|
|
13076
13369
|
checklist
|
|
@@ -17215,6 +17508,32 @@ var init_headless_boundaries = __esm(() => {
|
|
|
17215
17508
|
LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]);
|
|
17216
17509
|
});
|
|
17217
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
|
+
|
|
17218
17537
|
// src/lib/doctor.ts
|
|
17219
17538
|
var exports_doctor = {};
|
|
17220
17539
|
__export(exports_doctor, {
|
|
@@ -17222,7 +17541,7 @@ __export(exports_doctor, {
|
|
|
17222
17541
|
});
|
|
17223
17542
|
import { chmodSync, copyFileSync, existsSync as existsSync7, mkdirSync as mkdirSync5, statSync as statSync3 } from "fs";
|
|
17224
17543
|
import { basename as basename2, dirname as dirname5, join as join6 } from "path";
|
|
17225
|
-
function
|
|
17544
|
+
function tableExists2(db, table) {
|
|
17226
17545
|
return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
|
|
17227
17546
|
}
|
|
17228
17547
|
function quoteIdent(identifier) {
|
|
@@ -17306,7 +17625,7 @@ function findDuplicateIndexes(db) {
|
|
|
17306
17625
|
return duplicates;
|
|
17307
17626
|
}
|
|
17308
17627
|
function findMissingProjectRoots(db) {
|
|
17309
|
-
if (!
|
|
17628
|
+
if (!tableExists2(db, "projects"))
|
|
17310
17629
|
return 0;
|
|
17311
17630
|
let missing = 0;
|
|
17312
17631
|
const rows = db.query("SELECT path FROM projects WHERE path IS NOT NULL AND path != ''").all();
|
|
@@ -17321,7 +17640,7 @@ function findMissingProjectRoots(db) {
|
|
|
17321
17640
|
return missing;
|
|
17322
17641
|
}
|
|
17323
17642
|
function addTaskStateChecks(db, checks) {
|
|
17324
|
-
if (!
|
|
17643
|
+
if (!tableExists2(db, "tasks"))
|
|
17325
17644
|
return;
|
|
17326
17645
|
const columns = getTableColumns(db, "tasks");
|
|
17327
17646
|
const staleCutoff = new Date(Date.now() - 30 * 60 * 1000).toISOString();
|
|
@@ -17393,15 +17712,42 @@ function createBackup(dbPath) {
|
|
|
17393
17712
|
function pushRepair(repairs, type, message, applied, count) {
|
|
17394
17713
|
repairs.push({ type, message, applied, count });
|
|
17395
17714
|
}
|
|
17396
|
-
function summarize2(checks, repairs) {
|
|
17715
|
+
function summarize2(checks, repairs, integrity) {
|
|
17397
17716
|
return {
|
|
17398
17717
|
errors: checks.filter((check) => check.severity === "error").length,
|
|
17399
17718
|
warnings: checks.filter((check) => check.severity === "warn").length,
|
|
17400
17719
|
infos: checks.filter((check) => check.severity === "info").length,
|
|
17401
17720
|
repairable: checks.filter((check) => check.repairable).length,
|
|
17402
|
-
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
|
|
17403
17725
|
};
|
|
17404
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
|
+
}
|
|
17405
17751
|
function deleteOrphans(db, table, where) {
|
|
17406
17752
|
const before = countQuery(db, `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`);
|
|
17407
17753
|
if (before > 0)
|
|
@@ -17430,7 +17776,7 @@ function runTodosDoctor(options = {}) {
|
|
|
17430
17776
|
message: `Schema at migration ${migrationCurrent}`
|
|
17431
17777
|
});
|
|
17432
17778
|
}
|
|
17433
|
-
const missingTables =
|
|
17779
|
+
const missingTables = REQUIRED_TABLES2.filter((table) => !tableExists2(db, table));
|
|
17434
17780
|
if (missingTables.length > 0) {
|
|
17435
17781
|
addCheck(checks, {
|
|
17436
17782
|
severity: "error",
|
|
@@ -17440,7 +17786,7 @@ function runTodosDoctor(options = {}) {
|
|
|
17440
17786
|
repairable: true
|
|
17441
17787
|
});
|
|
17442
17788
|
}
|
|
17443
|
-
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;
|
|
17444
17790
|
if (orphanedParents > 0) {
|
|
17445
17791
|
addCheck(checks, {
|
|
17446
17792
|
severity: "error",
|
|
@@ -17450,7 +17796,7 @@ function runTodosDoctor(options = {}) {
|
|
|
17450
17796
|
repairable: true
|
|
17451
17797
|
});
|
|
17452
17798
|
}
|
|
17453
|
-
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;
|
|
17454
17800
|
if (orphanedDependencies > 0) {
|
|
17455
17801
|
addCheck(checks, {
|
|
17456
17802
|
severity: "error",
|
|
@@ -17469,7 +17815,7 @@ function runTodosDoctor(options = {}) {
|
|
|
17469
17815
|
];
|
|
17470
17816
|
let orphanedRows = 0;
|
|
17471
17817
|
for (const [table, where] of orphanTables) {
|
|
17472
|
-
if (!
|
|
17818
|
+
if (!tableExists2(db, table))
|
|
17473
17819
|
continue;
|
|
17474
17820
|
orphanedRows += countQuery(db, `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`);
|
|
17475
17821
|
}
|
|
@@ -17483,6 +17829,8 @@ function runTodosDoctor(options = {}) {
|
|
|
17483
17829
|
});
|
|
17484
17830
|
}
|
|
17485
17831
|
addTaskStateChecks(db, checks);
|
|
17832
|
+
const integrity = scanSqliteIntegrity(db);
|
|
17833
|
+
addIntegrityChecks(integrity, checks);
|
|
17486
17834
|
const corruptJson = findCorruptJsonMetadata(db);
|
|
17487
17835
|
if (corruptJson.length > 0) {
|
|
17488
17836
|
addCheck(checks, {
|
|
@@ -17541,12 +17889,12 @@ function runTodosDoctor(options = {}) {
|
|
|
17541
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)");
|
|
17542
17890
|
pushRepair(repairs, "orphaned_task_parents", "Cleared missing parent references", true, orphanedParents);
|
|
17543
17891
|
}
|
|
17544
|
-
if (orphanedDependencies > 0 &&
|
|
17892
|
+
if (orphanedDependencies > 0 && tableExists2(db, "task_dependencies")) {
|
|
17545
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)");
|
|
17546
17894
|
pushRepair(repairs, "orphaned_task_dependencies", "Deleted dependency rows referencing missing tasks", true, count);
|
|
17547
17895
|
}
|
|
17548
17896
|
for (const [table, where] of orphanTables) {
|
|
17549
|
-
if (!
|
|
17897
|
+
if (!tableExists2(db, table))
|
|
17550
17898
|
continue;
|
|
17551
17899
|
const count = deleteOrphans(db, table, where);
|
|
17552
17900
|
if (count > 0)
|
|
@@ -17575,26 +17923,30 @@ function runTodosDoctor(options = {}) {
|
|
|
17575
17923
|
}
|
|
17576
17924
|
}
|
|
17577
17925
|
}
|
|
17578
|
-
const
|
|
17579
|
-
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);
|
|
17580
17930
|
return {
|
|
17581
|
-
ok: !finalChecks.some((check) => check.severity === "error"),
|
|
17931
|
+
ok: !finalChecks.some((check) => check.severity === "error") && finalIntegrity.summary.ok,
|
|
17582
17932
|
dry_run: !apply,
|
|
17583
17933
|
database_path: dbPath,
|
|
17584
17934
|
migration: { current: getMigrationLevel(db), expected: migrationExpected },
|
|
17585
17935
|
backup,
|
|
17586
17936
|
checks: finalChecks,
|
|
17587
17937
|
repairs,
|
|
17938
|
+
integrity: finalIntegrity,
|
|
17588
17939
|
summary
|
|
17589
17940
|
};
|
|
17590
17941
|
}
|
|
17591
|
-
var
|
|
17942
|
+
var REQUIRED_TABLES2;
|
|
17592
17943
|
var init_doctor = __esm(() => {
|
|
17593
17944
|
init_database();
|
|
17945
|
+
init_integrity2();
|
|
17594
17946
|
init_migrations();
|
|
17595
17947
|
init_schema();
|
|
17596
17948
|
init_recurrence();
|
|
17597
|
-
|
|
17949
|
+
REQUIRED_TABLES2 = [
|
|
17598
17950
|
"_migrations",
|
|
17599
17951
|
"projects",
|
|
17600
17952
|
"tasks",
|
|
@@ -18986,15 +19338,15 @@ function resolveTaskRefLocal(db, ref) {
|
|
|
18986
19338
|
return null;
|
|
18987
19339
|
if (TASK_UUID_RE.test(raw))
|
|
18988
19340
|
return getTask(raw, db);
|
|
18989
|
-
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}`)}%`);
|
|
18990
19342
|
if (prefixRows.length > 1) {
|
|
18991
|
-
throw new
|
|
19343
|
+
throw new TaskReferenceAmbiguousError(ref, prefixRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
|
|
18992
19344
|
}
|
|
18993
19345
|
if (prefixRows.length === 1)
|
|
18994
19346
|
return getTask(prefixRows[0].id, db);
|
|
18995
|
-
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);
|
|
18996
19348
|
if (shortIdRows.length > 1) {
|
|
18997
|
-
throw new
|
|
19349
|
+
throw new TaskReferenceAmbiguousError(ref, shortIdRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
|
|
18998
19350
|
}
|
|
18999
19351
|
if (shortIdRows.length === 1)
|
|
19000
19352
|
return getTask(shortIdRows[0].id, db);
|
|
@@ -19143,6 +19495,9 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
19143
19495
|
exportSnapshot: () => exportSqliteTodosStorageSnapshot(database()),
|
|
19144
19496
|
importSnapshot: (snapshot) => importSqliteTodosStorageSnapshot(snapshot, database())
|
|
19145
19497
|
},
|
|
19498
|
+
integrity: {
|
|
19499
|
+
report: () => scanSqliteIntegrity(database())
|
|
19500
|
+
},
|
|
19146
19501
|
transaction: (fn) => {
|
|
19147
19502
|
const tx = database().transaction(() => fn(adapter));
|
|
19148
19503
|
return tx();
|
|
@@ -19152,6 +19507,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
19152
19507
|
}
|
|
19153
19508
|
var TASK_UUID_RE;
|
|
19154
19509
|
var init_local_sqlite = __esm(() => {
|
|
19510
|
+
init_types();
|
|
19155
19511
|
init_search();
|
|
19156
19512
|
init_tasks();
|
|
19157
19513
|
init_projects();
|
|
@@ -19162,6 +19518,7 @@ var init_local_sqlite = __esm(() => {
|
|
|
19162
19518
|
init_audit();
|
|
19163
19519
|
init_comments();
|
|
19164
19520
|
init_database();
|
|
19521
|
+
init_integrity2();
|
|
19165
19522
|
init_sqlite_snapshot();
|
|
19166
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;
|
|
19167
19524
|
});
|
|
@@ -20460,6 +20817,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
20460
20817
|
{ name: "task_list_id", in: "query", schema: { type: "string" } },
|
|
20461
20818
|
{ name: "assigned_to", in: "query", schema: { type: "string" } },
|
|
20462
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" },
|
|
20463
20821
|
{ name: "limit", in: "query", schema: { type: "integer", minimum: 1 } },
|
|
20464
20822
|
{ name: "offset", in: "query", schema: { type: "integer", minimum: 0 } }
|
|
20465
20823
|
],
|
|
@@ -21664,6 +22022,9 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
21664
22022
|
...url.searchParams.get("task_list_id") ? { task_list_id: url.searchParams.get("task_list_id") } : {},
|
|
21665
22023
|
...url.searchParams.get("assigned_to") ? { assigned_to: url.searchParams.get("assigned_to") } : {},
|
|
21666
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
|
+
} : {},
|
|
21667
22028
|
...url.searchParams.get("limit") ? { limit: Number(url.searchParams.get("limit")) } : {},
|
|
21668
22029
|
...url.searchParams.get("offset") ? { offset: Number(url.searchParams.get("offset")) } : {}
|
|
21669
22030
|
};
|
|
@@ -21945,6 +22306,13 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
21945
22306
|
try {
|
|
21946
22307
|
task = await store.tasks.resolveRef(id);
|
|
21947
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
|
+
}
|
|
21948
22316
|
const msg = e.message || "";
|
|
21949
22317
|
if (/ambiguous/i.test(msg))
|
|
21950
22318
|
return error(409, msg);
|
|
@@ -22302,6 +22670,15 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
22302
22670
|
]);
|
|
22303
22671
|
return json3({ tasks, tasks_all: tasksAll, subtasks: tasksAll - tasks, projects: projects.length });
|
|
22304
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
|
+
}
|
|
22305
22682
|
if (resource === "import") {
|
|
22306
22683
|
if (method !== "POST")
|
|
22307
22684
|
return error(405, `method ${method} not allowed on /v1/import`);
|
|
@@ -22321,6 +22698,13 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
22321
22698
|
}
|
|
22322
22699
|
return error(404, `unknown /v1 resource: ${resource ?? "(root)"}`);
|
|
22323
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
|
+
}
|
|
22324
22708
|
if (e instanceof LockError)
|
|
22325
22709
|
return error(409, e.message, { code: LockError.code });
|
|
22326
22710
|
if (e instanceof ResourceConflictError)
|
|
@@ -44653,9 +45037,6 @@ function cleanMode(value) {
|
|
|
44653
45037
|
const normalized = value?.trim().toLowerCase();
|
|
44654
45038
|
return normalized || null;
|
|
44655
45039
|
}
|
|
44656
|
-
function modeRole(mode) {
|
|
44657
|
-
return mode === "local" ? "local" : "remote";
|
|
44658
|
-
}
|
|
44659
45040
|
function resolveTodosCliStorageMode(env = process.env) {
|
|
44660
45041
|
for (const source of ["HASNA_TODOS_STORAGE_MODE", "TODOS_STORAGE_MODE"]) {
|
|
44661
45042
|
if (env[source] !== undefined && env[source].trim() === "") {
|
|
@@ -44668,22 +45049,25 @@ function resolveTodosCliStorageMode(env = process.env) {
|
|
|
44668
45049
|
["HASNA_TODOS_STORAGE_MODE", canonical],
|
|
44669
45050
|
["TODOS_STORAGE_MODE", fallback]
|
|
44670
45051
|
]) {
|
|
44671
|
-
if (value && !
|
|
44672
|
-
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");
|
|
44673
45054
|
}
|
|
44674
45055
|
}
|
|
44675
|
-
|
|
45056
|
+
const canonicalTransport = canonical ? TRANSPORT_TOKENS[canonical] : null;
|
|
45057
|
+
const fallbackTransport = fallback ? TRANSPORT_TOKENS[fallback] : null;
|
|
45058
|
+
if (canonicalTransport && fallbackTransport && canonicalTransport !== fallbackTransport) {
|
|
44676
45059
|
throw new Error(`REMOTE_STORAGE_MODE_CONFLICT: HASNA_TODOS_STORAGE_MODE=${canonical} conflicts with ` + `TODOS_STORAGE_MODE=${fallback}; local SQLite fallback is disabled`);
|
|
44677
45060
|
}
|
|
44678
|
-
const
|
|
45061
|
+
const transport = canonicalTransport ?? fallbackTransport ?? "sqlite";
|
|
44679
45062
|
return {
|
|
44680
|
-
mode,
|
|
44681
|
-
|
|
45063
|
+
mode: transport,
|
|
45064
|
+
transport,
|
|
45065
|
+
selected: transport === "http",
|
|
44682
45066
|
source: canonical ? "HASNA_TODOS_STORAGE_MODE" : fallback ? "TODOS_STORAGE_MODE" : "default"
|
|
44683
45067
|
};
|
|
44684
45068
|
}
|
|
44685
|
-
function
|
|
44686
|
-
return resolveTodosCliStorageMode(env).
|
|
45069
|
+
function requestedTransport(env) {
|
|
45070
|
+
return resolveTodosCliStorageMode(env).transport;
|
|
44687
45071
|
}
|
|
44688
45072
|
function normalizeRemoteAuthorityUrl(value) {
|
|
44689
45073
|
const trimmed = value?.trim();
|
|
@@ -44736,7 +45120,7 @@ function getTodosRemoteAuthorityConfigStatus(env = process.env) {
|
|
|
44736
45120
|
return {
|
|
44737
45121
|
selected: false,
|
|
44738
45122
|
ok: true,
|
|
44739
|
-
mode
|
|
45123
|
+
mode,
|
|
44740
45124
|
api_url_configured: false,
|
|
44741
45125
|
api_key_configured: false,
|
|
44742
45126
|
v1_base_url: null,
|
|
@@ -44848,8 +45232,7 @@ async function requiredRemoteRoute(client, route, request) {
|
|
|
44848
45232
|
}
|
|
44849
45233
|
}
|
|
44850
45234
|
function getTodosCloudClient(env = process.env) {
|
|
44851
|
-
|
|
44852
|
-
if (!CLOUD_MODES.has(mode))
|
|
45235
|
+
if (requestedTransport(env) !== "http")
|
|
44853
45236
|
return null;
|
|
44854
45237
|
const resolved = resolveStorageClient("todos", requireTodosRemoteAuthorityEnv(env), {
|
|
44855
45238
|
fetchImpl: (input, init) => globalThis.fetch(input, { ...init, redirect: "manual" })
|
|
@@ -44884,13 +45267,38 @@ function toListQuery(filter = {}) {
|
|
|
44884
45267
|
query["assigned_to"] = filter.assigned_to;
|
|
44885
45268
|
if (filter.agent_id)
|
|
44886
45269
|
query["agent_id"] = filter.agent_id;
|
|
45270
|
+
if (filter.tags?.length)
|
|
45271
|
+
query["tags"] = filter.tags.join(",");
|
|
44887
45272
|
if (typeof filter.limit === "number")
|
|
44888
45273
|
query["limit"] = filter.limit;
|
|
44889
45274
|
if (typeof filter.offset === "number")
|
|
44890
45275
|
query["offset"] = filter.offset;
|
|
44891
45276
|
return query;
|
|
44892
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
|
+
}
|
|
44893
45299
|
async function cloudListTasks(client, filter = {}) {
|
|
45300
|
+
if (filter.tags?.length)
|
|
45301
|
+
await requireTagsFilterCapability(client);
|
|
44894
45302
|
const res = await requiredRemoteRoute(client, "/v1/tasks", () => client.list("tasks", { query: toListQuery(filter) }));
|
|
44895
45303
|
const envelope = res.raw;
|
|
44896
45304
|
return Array.isArray(envelope?.tasks) ? envelope.tasks : res.items;
|
|
@@ -44998,6 +45406,8 @@ function redactComment3(comment) {
|
|
|
44998
45406
|
}
|
|
44999
45407
|
async function cloudCountTasks(client, filter = {}) {
|
|
45000
45408
|
const { limit: _drop, offset: _o, ...rest } = filter;
|
|
45409
|
+
if (rest.tags?.length)
|
|
45410
|
+
await requireTagsFilterCapability(client);
|
|
45001
45411
|
const res = await requiredRemoteRoute(client, "/v1/tasks", () => client.list("tasks", { query: { ...toListQuery(rest), limit: 1 } }));
|
|
45002
45412
|
const envelope = res.raw;
|
|
45003
45413
|
if (typeof envelope?.total === "number")
|
|
@@ -45060,14 +45470,23 @@ async function cloudResolveTaskListRef(client, ref, projectId) {
|
|
|
45060
45470
|
}
|
|
45061
45471
|
throw new Error(`Task list not found: "${input}"`);
|
|
45062
45472
|
}
|
|
45063
|
-
var UUID_RE,
|
|
45473
|
+
var UUID_RE, TRANSPORT_TOKENS, completionCapabilityCache, listTagsCapabilityCache;
|
|
45064
45474
|
var init_cloud_router = __esm(() => {
|
|
45475
|
+
init_types();
|
|
45065
45476
|
init_redaction();
|
|
45066
45477
|
init_http_client();
|
|
45067
45478
|
UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
45068
|
-
|
|
45069
|
-
|
|
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
|
+
};
|
|
45070
45488
|
completionCapabilityCache = new Map;
|
|
45489
|
+
listTagsCapabilityCache = new Map;
|
|
45071
45490
|
});
|
|
45072
45491
|
|
|
45073
45492
|
// src/mcp/tools/task-crud.ts
|
|
@@ -48653,12 +49072,11 @@ ${task.description || ""}`.split(/\r?\n/);
|
|
|
48653
49072
|
function normalizeStackLocation(value) {
|
|
48654
49073
|
return value.replace(/:\d+(?::\d+)?/g, "").replace(/^file:\/\//, "").toLowerCase().trim();
|
|
48655
49074
|
}
|
|
48656
|
-
function
|
|
49075
|
+
function fingerprintContent(task) {
|
|
48657
49076
|
const body = task.description || "";
|
|
48658
49077
|
const text = `${task.title}
|
|
48659
49078
|
${body}`;
|
|
48660
49079
|
return {
|
|
48661
|
-
task,
|
|
48662
49080
|
title: normalizeText(task.title),
|
|
48663
49081
|
body: normalizeText(body),
|
|
48664
49082
|
text: normalizeText(text),
|
|
@@ -48667,6 +49085,9 @@ ${body}`;
|
|
|
48667
49085
|
stackKeys: stackKeysFor(task)
|
|
48668
49086
|
};
|
|
48669
49087
|
}
|
|
49088
|
+
function fingerprint(task) {
|
|
49089
|
+
return { task, ...fingerprintContent(task) };
|
|
49090
|
+
}
|
|
48670
49091
|
function intersects(left, right) {
|
|
48671
49092
|
for (const value of left)
|
|
48672
49093
|
if (right.has(value))
|
|
@@ -49875,7 +50296,7 @@ function tableColumns(db, table) {
|
|
|
49875
50296
|
}
|
|
49876
50297
|
function assertMigratedSchema(db) {
|
|
49877
50298
|
const issues = [];
|
|
49878
|
-
for (const table of
|
|
50299
|
+
for (const table of REQUIRED_TABLES3) {
|
|
49879
50300
|
if (!hasTable(db, table))
|
|
49880
50301
|
issues.push(`missing table ${table}`);
|
|
49881
50302
|
}
|
|
@@ -49983,7 +50404,7 @@ function createReleaseCompatibilityReport(options = {}) {
|
|
|
49983
50404
|
migrations: {
|
|
49984
50405
|
current_level: MIGRATIONS.length,
|
|
49985
50406
|
simulated_levels: simulatedLevels,
|
|
49986
|
-
checked_tables:
|
|
50407
|
+
checked_tables: REQUIRED_TABLES3,
|
|
49987
50408
|
checked_columns: REQUIRED_COLUMNS
|
|
49988
50409
|
},
|
|
49989
50410
|
exports: {
|
|
@@ -50003,7 +50424,7 @@ function createReleaseCompatibilityReport(options = {}) {
|
|
|
50003
50424
|
"todos --help",
|
|
50004
50425
|
"todos-mcp --help",
|
|
50005
50426
|
"todos-serve --help",
|
|
50006
|
-
"todos doctor"
|
|
50427
|
+
"todos doctor --no-fail-on-findings"
|
|
50007
50428
|
],
|
|
50008
50429
|
smoke_tests: [
|
|
50009
50430
|
"command -v todos",
|
|
@@ -50011,7 +50432,7 @@ function createReleaseCompatibilityReport(options = {}) {
|
|
|
50011
50432
|
"command -v todos-serve",
|
|
50012
50433
|
"todos --version",
|
|
50013
50434
|
"todos --help",
|
|
50014
|
-
"todos doctor"
|
|
50435
|
+
"todos doctor --no-fail-on-findings"
|
|
50015
50436
|
],
|
|
50016
50437
|
rollback: [
|
|
50017
50438
|
"npm view @hasna/todos versions --json",
|
|
@@ -50060,14 +50481,14 @@ function renderReleaseCompatibilityMarkdown(report) {
|
|
|
50060
50481
|
return lines.join(`
|
|
50061
50482
|
`);
|
|
50062
50483
|
}
|
|
50063
|
-
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;
|
|
50064
50485
|
var init_release_compatibility = __esm(() => {
|
|
50065
50486
|
init_migrations();
|
|
50066
50487
|
init_schema();
|
|
50067
50488
|
EXPECTED_BINS = ["todos", "todos-mcp", "todos-serve"];
|
|
50068
50489
|
EXPECTED_EXPORTS = [".", "./sdk", "./mcp", "./registry", "./contracts", "./storage"];
|
|
50069
50490
|
REQUIRED_SCRIPTS = ["build", "test:no-cloud", "verify:release", "prepublishOnly"];
|
|
50070
|
-
|
|
50491
|
+
REQUIRED_TABLES3 = [
|
|
50071
50492
|
"_migrations",
|
|
50072
50493
|
"projects",
|
|
50073
50494
|
"tasks",
|
|
@@ -53826,7 +54247,7 @@ function limits(input) {
|
|
|
53826
54247
|
verification_limit: clamp(input.verification_limit, DEFAULT_LIMITS.verification_limit, 100),
|
|
53827
54248
|
run_limit: clamp(input.run_limit, DEFAULT_LIMITS.run_limit, 20),
|
|
53828
54249
|
dependency_limit: clamp(input.dependency_limit, DEFAULT_LIMITS.dependency_limit, 100),
|
|
53829
|
-
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),
|
|
53830
54251
|
max_text_chars: clamp(input.max_text_chars, DEFAULT_LIMITS.max_text_chars, 50000),
|
|
53831
54252
|
summary_char_limit: clamp(input.summary_char_limit, DEFAULT_LIMITS.summary_char_limit, 4000),
|
|
53832
54253
|
stale_after_hours: clamp(input.stale_after_hours, DEFAULT_LIMITS.stale_after_hours, 24 * 365)
|
|
@@ -53849,6 +54270,13 @@ function taskSummary(task) {
|
|
|
53849
54270
|
priority: task.priority
|
|
53850
54271
|
};
|
|
53851
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
|
+
}
|
|
53852
54280
|
function acceptanceCriteria(task, maxText) {
|
|
53853
54281
|
const metadata = task.metadata || {};
|
|
53854
54282
|
const raw = metadata["acceptance_criteria"] ?? metadata["acceptanceCriteria"] ?? metadata["criteria"];
|
|
@@ -53946,7 +54374,7 @@ function summarizeSection(pack, section, maxChars) {
|
|
|
53946
54374
|
return "No plan was attached.";
|
|
53947
54375
|
return summarizeStrings([
|
|
53948
54376
|
`Plan ${pack.plan.name} is ${pack.plan.status}`,
|
|
53949
|
-
`${pack.plan.tasks.length} listed plan
|
|
54377
|
+
`${pack.plan.tasks.length} of ${pack.plan.total_tasks} plan tasks listed; ${pack.plan.omitted_tasks} omitted`,
|
|
53950
54378
|
...pack.plan.tasks.slice(0, 4).map((task) => `${task.status} ${task.short_id || task.id.slice(0, 8)} ${task.title}`)
|
|
53951
54379
|
], maxChars);
|
|
53952
54380
|
}
|
|
@@ -54096,7 +54524,8 @@ function createAgentContextPack(input, db) {
|
|
|
54096
54524
|
const limit = limits(input);
|
|
54097
54525
|
const project = task.project_id ? getProject(task.project_id, d) : null;
|
|
54098
54526
|
const plan = task.plan_id ? getPlan(task.plan_id, d) : null;
|
|
54099
|
-
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;
|
|
54100
54529
|
const upstream = getTaskDependencies(task.id, d).map((dep) => taskSummary(getTask(dep.depends_on, d))).filter((item) => Boolean(item));
|
|
54101
54530
|
const downstream = getTaskDependents(task.id, d).map((dep) => taskSummary(getTask(dep.task_id, d))).filter((item) => Boolean(item));
|
|
54102
54531
|
const comments = listComments(task.id, d);
|
|
@@ -54139,6 +54568,9 @@ function createAgentContextPack(input, db) {
|
|
|
54139
54568
|
warnings.push(`${runs.length - selectedRuns.length} older runs omitted`);
|
|
54140
54569
|
if (fileMap.size > relevantFiles.length)
|
|
54141
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
|
+
}
|
|
54142
54574
|
const contextTask = {
|
|
54143
54575
|
id: task.id,
|
|
54144
54576
|
short_id: task.short_id,
|
|
@@ -54170,8 +54602,9 @@ function createAgentContextPack(input, db) {
|
|
|
54170
54602
|
description: truncate2(plan.description, limit.max_text_chars),
|
|
54171
54603
|
status: plan.status,
|
|
54172
54604
|
agent_id: plan.agent_id,
|
|
54173
|
-
tasks: planTasks.
|
|
54174
|
-
|
|
54605
|
+
tasks: planTasks.map(taskSummary).filter((item) => Boolean(item)),
|
|
54606
|
+
total_tasks: totalPlanTasks,
|
|
54607
|
+
omitted_tasks: Math.max(0, totalPlanTasks - planTasks.length)
|
|
54175
54608
|
} : null,
|
|
54176
54609
|
acceptance_criteria: acceptanceCriteria(task, limit.max_text_chars),
|
|
54177
54610
|
dependencies: {
|
|
@@ -54281,6 +54714,8 @@ ${pack.task.description}` : null,
|
|
|
54281
54714
|
"## Project And Plan",
|
|
54282
54715
|
pack.project ? `- Project: ${pack.project.name} (${pack.project.path})` : "- Project: none",
|
|
54283
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,
|
|
54284
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,
|
|
54285
54720
|
"",
|
|
54286
54721
|
"## Acceptance Criteria",
|
|
@@ -54361,7 +54796,7 @@ function renderAgentContextPack(pack, format, compact2 = false) {
|
|
|
54361
54796
|
return compact2 ? renderAgentContextPackCompactMarkdown(pack) : renderAgentContextPackMarkdown(pack);
|
|
54362
54797
|
return compact2 ? JSON.stringify(pack) : JSON.stringify(pack, null, 2);
|
|
54363
54798
|
}
|
|
54364
|
-
var DEFAULT_LIMITS, ALL_CONTEXT_SECTIONS, BUDGET_TRIM_ORDER;
|
|
54799
|
+
var DEFAULT_LIMITS, MAX_CONTEXT_TASKS = 24, ALL_CONTEXT_SECTIONS, BUDGET_TRIM_ORDER;
|
|
54365
54800
|
var init_context_packs = __esm(() => {
|
|
54366
54801
|
init_comments();
|
|
54367
54802
|
init_database();
|
|
@@ -54380,7 +54815,7 @@ var init_context_packs = __esm(() => {
|
|
|
54380
54815
|
verification_limit: 10,
|
|
54381
54816
|
run_limit: 3,
|
|
54382
54817
|
dependency_limit: 12,
|
|
54383
|
-
plan_task_limit:
|
|
54818
|
+
plan_task_limit: 24,
|
|
54384
54819
|
max_text_chars: 6000,
|
|
54385
54820
|
summary_char_limit: 480,
|
|
54386
54821
|
stale_after_hours: 72
|
|
@@ -79203,7 +79638,7 @@ function parseObject2(value) {
|
|
|
79203
79638
|
function normalizeKey(value) {
|
|
79204
79639
|
return value.trim().toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
79205
79640
|
}
|
|
79206
|
-
function
|
|
79641
|
+
function normalizeTaskFindingFingerprint(value) {
|
|
79207
79642
|
const normalized = normalizeKey(value);
|
|
79208
79643
|
if (!normalized)
|
|
79209
79644
|
throw new Error("finding fingerprint is required");
|
|
@@ -79312,7 +79747,7 @@ function assertTask(taskId, db) {
|
|
|
79312
79747
|
throw new TaskNotFoundError(taskId);
|
|
79313
79748
|
}
|
|
79314
79749
|
function nextFinding(input, db) {
|
|
79315
|
-
const fingerprint2 =
|
|
79750
|
+
const fingerprint2 = normalizeTaskFindingFingerprint(input.fingerprint);
|
|
79316
79751
|
const title = redactOptional(input.title, 300);
|
|
79317
79752
|
if (!title)
|
|
79318
79753
|
throw new Error("finding title is required");
|
|
@@ -79460,7 +79895,7 @@ function resolveMissingTaskFindings(input, db) {
|
|
|
79460
79895
|
const timestamp4 = input.resolved_at || now();
|
|
79461
79896
|
const status = normalizeResolutionStatus(input.status);
|
|
79462
79897
|
const runId = resolveRunForTask(input.run_id, input.task_id, d);
|
|
79463
|
-
const present = new Set(input.fingerprints.map(
|
|
79898
|
+
const present = new Set(input.fingerprints.map(normalizeTaskFindingFingerprint));
|
|
79464
79899
|
const warnings = [];
|
|
79465
79900
|
const conditions = ["task_id = ?", "status = 'open'"];
|
|
79466
79901
|
const params = [input.task_id];
|
|
@@ -91205,6 +91640,15 @@ function formatError2(error3) {
|
|
|
91205
91640
|
if (error3 instanceof TaskNotFoundError) {
|
|
91206
91641
|
return JSON.stringify({ code: TaskNotFoundError.code, message: error3.message, suggestion: TaskNotFoundError.suggestion });
|
|
91207
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
|
+
}
|
|
91208
91652
|
if (error3 instanceof ProjectNotFoundError) {
|
|
91209
91653
|
return JSON.stringify({ code: ProjectNotFoundError.code, message: error3.message, suggestion: ProjectNotFoundError.suggestion });
|
|
91210
91654
|
}
|
|
@@ -91977,7 +92421,7 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
91977
92421
|
const path = url.pathname;
|
|
91978
92422
|
const method = req.method;
|
|
91979
92423
|
const reqOrigin = req.headers.get("origin") || undefined;
|
|
91980
|
-
const corsHeaders = reqOrigin &&
|
|
92424
|
+
const corsHeaders = reqOrigin && reqOrigin === `http://localhost:${ctx.port}` ? {
|
|
91981
92425
|
"Access-Control-Allow-Origin": reqOrigin,
|
|
91982
92426
|
"Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE, OPTIONS",
|
|
91983
92427
|
"Access-Control-Allow-Headers": "Content-Type, X-API-Key, Authorization",
|
|
@@ -92002,23 +92446,24 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
92002
92446
|
}
|
|
92003
92447
|
if ((path === "/health" || path === "/ready" || path === "/version") && method === "GET") {
|
|
92004
92448
|
const { getPackageVersion: getPackageVersion2 } = await Promise.resolve().then(() => (init_package_version(), exports_package_version));
|
|
92005
|
-
const {
|
|
92006
|
-
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";
|
|
92007
92452
|
const version2 = getPackageVersion2();
|
|
92008
92453
|
if (path === "/version") {
|
|
92009
|
-
return Response.json({ status: "ok", version: version2, mode, name: "todos" });
|
|
92454
|
+
return Response.json({ status: "ok", version: version2, mode, backend, name: "todos" });
|
|
92010
92455
|
}
|
|
92011
92456
|
if (path === "/ready") {
|
|
92012
|
-
if (
|
|
92457
|
+
if (backend === "postgresql") {
|
|
92013
92458
|
try {
|
|
92014
92459
|
await pingCloud2();
|
|
92015
92460
|
} catch (e) {
|
|
92016
|
-
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 });
|
|
92017
92462
|
}
|
|
92018
92463
|
}
|
|
92019
|
-
return Response.json({ status: "ready", version: version2, mode });
|
|
92464
|
+
return Response.json({ status: "ready", version: version2, mode, backend });
|
|
92020
92465
|
}
|
|
92021
|
-
return Response.json({ status: "ok", version: version2, mode, name: "todos" });
|
|
92466
|
+
return Response.json({ status: "ok", version: version2, mode, backend, name: "todos" });
|
|
92022
92467
|
}
|
|
92023
92468
|
if ((path === "/openapi.json" || path === "/v1/openapi.json") && method === "GET") {
|
|
92024
92469
|
const { buildV1OpenApiDocument: buildV1OpenApiDocument2 } = await Promise.resolve().then(() => (init_openapi(), exports_openapi));
|
|
@@ -92265,7 +92710,9 @@ Dashboard not found at: ${dashboardDir}`);
|
|
|
92265
92710
|
};
|
|
92266
92711
|
process.on("SIGINT", shutdown);
|
|
92267
92712
|
process.on("SIGTERM", shutdown);
|
|
92268
|
-
const
|
|
92713
|
+
const boundPort = server.port ?? port;
|
|
92714
|
+
ctx.port = boundPort;
|
|
92715
|
+
const serverUrl = `http://localhost:${boundPort}`;
|
|
92269
92716
|
console.log(`Todos Dashboard running at ${serverUrl}`);
|
|
92270
92717
|
if (shouldOpen) {
|
|
92271
92718
|
try {
|
|
@@ -92306,7 +92753,39 @@ var init_serve = __esm(() => {
|
|
|
92306
92753
|
|
|
92307
92754
|
// src/server/index.ts
|
|
92308
92755
|
init_package_version();
|
|
92756
|
+
|
|
92757
|
+
// src/server/port.ts
|
|
92309
92758
|
var DEFAULT_PORT = 19427;
|
|
92759
|
+
function coercePort(raw) {
|
|
92760
|
+
if (raw === undefined)
|
|
92761
|
+
return;
|
|
92762
|
+
const trimmed = raw.trim();
|
|
92763
|
+
if (!/^\d+$/.test(trimmed))
|
|
92764
|
+
return;
|
|
92765
|
+
if (trimmed.length > 1 && trimmed.startsWith("0"))
|
|
92766
|
+
return;
|
|
92767
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
92768
|
+
if (!Number.isInteger(parsed) || parsed > 65535)
|
|
92769
|
+
return;
|
|
92770
|
+
return parsed;
|
|
92771
|
+
}
|
|
92772
|
+
function refuseInvalidPort(source, raw) {
|
|
92773
|
+
console.error(`Invalid ${source}: ${JSON.stringify(raw)} is not a port.
|
|
92774
|
+
` + `Use an integer from 0 to 65535, where 0 asks the kernel for a free port.`);
|
|
92775
|
+
process.exit(1);
|
|
92776
|
+
}
|
|
92777
|
+
async function findFreePort(start) {
|
|
92778
|
+
for (let port = start;port < start + 100; port++) {
|
|
92779
|
+
try {
|
|
92780
|
+
const server = Bun.serve({ port, fetch: () => new Response("") });
|
|
92781
|
+
server.stop(true);
|
|
92782
|
+
return port;
|
|
92783
|
+
} catch {}
|
|
92784
|
+
}
|
|
92785
|
+
return start;
|
|
92786
|
+
}
|
|
92787
|
+
|
|
92788
|
+
// src/server/index.ts
|
|
92310
92789
|
function hasVersionFlag2() {
|
|
92311
92790
|
return process.argv.includes("--version") || process.argv.includes("-V");
|
|
92312
92791
|
}
|
|
@@ -92341,11 +92820,8 @@ Environment:
|
|
|
92341
92820
|
function parsePort() {
|
|
92342
92821
|
const portArg = process.argv.find((a) => a === "--port" || a.startsWith("--port="));
|
|
92343
92822
|
if (portArg) {
|
|
92344
|
-
|
|
92345
|
-
|
|
92346
|
-
}
|
|
92347
|
-
const idx = process.argv.indexOf(portArg);
|
|
92348
|
-
return parseInt(process.argv[idx + 1], 10) || DEFAULT_PORT;
|
|
92823
|
+
const raw = portArg.includes("=") ? portArg.split("=")[1] : process.argv[process.argv.indexOf(portArg) + 1];
|
|
92824
|
+
return coercePort(raw) ?? refuseInvalidPort("--port", raw ?? "");
|
|
92349
92825
|
}
|
|
92350
92826
|
return DEFAULT_PORT;
|
|
92351
92827
|
}
|
|
@@ -92358,16 +92834,6 @@ function parseStringArg(name) {
|
|
|
92358
92834
|
const idx = process.argv.indexOf(arg);
|
|
92359
92835
|
return process.argv[idx + 1] || undefined;
|
|
92360
92836
|
}
|
|
92361
|
-
async function findFreePort(start) {
|
|
92362
|
-
for (let port = start;port < start + 100; port++) {
|
|
92363
|
-
try {
|
|
92364
|
-
const server = Bun.serve({ port, fetch: () => new Response("") });
|
|
92365
|
-
server.stop(true);
|
|
92366
|
-
return port;
|
|
92367
|
-
} catch {}
|
|
92368
|
-
}
|
|
92369
|
-
return start;
|
|
92370
|
-
}
|
|
92371
92837
|
async function runMigrate() {
|
|
92372
92838
|
const {
|
|
92373
92839
|
ensureCloudSchema: ensureCloudSchema2,
|
|
@@ -92463,9 +92929,10 @@ async function main2() {
|
|
|
92463
92929
|
return;
|
|
92464
92930
|
}
|
|
92465
92931
|
const explicitPortArg = process.argv.some((a) => a === "--port" || a.startsWith("--port="));
|
|
92466
|
-
const
|
|
92932
|
+
const rawEnvPort = process.env.PORT?.trim();
|
|
92933
|
+
const envPort = rawEnvPort ? coercePort(rawEnvPort) ?? refuseInvalidPort("PORT", rawEnvPort) : undefined;
|
|
92467
92934
|
const requestedPort = explicitPortArg ? parsePort() : envPort ?? parsePort();
|
|
92468
|
-
const port = envPort || explicitPortArg ? requestedPort : await findFreePort(requestedPort);
|
|
92935
|
+
const port = envPort !== undefined || explicitPortArg ? requestedPort : await findFreePort(requestedPort);
|
|
92469
92936
|
if (port !== requestedPort) {
|
|
92470
92937
|
console.log(`Port ${requestedPort} in use, using ${port}`);
|
|
92471
92938
|
}
|