@hasna/todos 0.13.2 → 0.13.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -14
- package/dist/cli/cloud-router.d.ts +57 -7
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/query-commands.d.ts.map +1 -1
- package/dist/cli/doctor-integrity.d.ts +106 -0
- package/dist/cli/doctor-integrity.d.ts.map +1 -0
- package/dist/cli/helpers.d.ts +2 -2
- package/dist/cli/index.js +1461 -778
- package/dist/cli/stage-a.d.ts.map +1 -1
- package/dist/contracts.js +118 -30
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/findings.d.ts +1 -0
- package/dist/db/findings.d.ts.map +1 -1
- package/dist/db/integrity.d.ts +20 -0
- package/dist/db/integrity.d.ts.map +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +562 -132
- package/dist/lib/context-packs.d.ts +1 -0
- package/dist/lib/context-packs.d.ts.map +1 -1
- package/dist/lib/dependency-graph.d.ts +1 -1
- package/dist/lib/doctor.d.ts +20 -0
- package/dist/lib/doctor.d.ts.map +1 -1
- package/dist/lib/integrity.d.ts +215 -0
- package/dist/lib/integrity.d.ts.map +1 -0
- package/dist/lib/native-storage-status.d.ts +2 -2
- package/dist/lib/native-storage-status.d.ts.map +1 -1
- package/dist/lib/release-compatibility.d.ts.map +1 -1
- package/dist/lib/task-dedupe.d.ts +15 -0
- package/dist/lib/task-dedupe.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +517 -85
- package/dist/mcp.js +7 -3
- package/dist/registry.d.ts +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +125 -30
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/types.d.ts +42 -0
- package/dist/sdk/types.d.ts.map +1 -1
- package/dist/sdk/v1.generated.d.ts +1 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/cloud.d.ts +8 -1
- package/dist/server/cloud.d.ts.map +1 -1
- package/dist/server/index.js +521 -89
- package/dist/server/openapi.d.ts +12 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/config.d.ts +23 -5
- package/dist/storage/config.d.ts.map +1 -1
- package/dist/storage/factory.d.ts +9 -3
- package/dist/storage/factory.d.ts.map +1 -1
- package/dist/storage/hybrid.d.ts +7 -0
- package/dist/storage/hybrid.d.ts.map +1 -1
- package/dist/storage/index.d.ts +3 -3
- package/dist/storage/index.d.ts.map +1 -1
- package/dist/storage/interfaces.d.ts +29 -8
- package/dist/storage/interfaces.d.ts.map +1 -1
- package/dist/storage/local-sqlite.d.ts.map +1 -1
- package/dist/storage/postgres-adapter.d.ts.map +1 -1
- package/dist/storage.d.ts +3 -3
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +601 -250
- package/dist/testing.d.ts +79 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +107 -0
- package/dist/types/index.d.ts +12 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +7 -3
package/dist/index.js
CHANGED
|
@@ -3427,7 +3427,7 @@ var init_machines = __esm(() => {
|
|
|
3427
3427
|
function isBlockingDependencyStatus(status) {
|
|
3428
3428
|
return status !== "completed" && status !== "cancelled";
|
|
3429
3429
|
}
|
|
3430
|
-
var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
|
|
3430
|
+
var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
|
|
3431
3431
|
var init_types = __esm(() => {
|
|
3432
3432
|
TASK_STATUSES = [
|
|
3433
3433
|
"pending",
|
|
@@ -3467,6 +3467,22 @@ var init_types = __esm(() => {
|
|
|
3467
3467
|
this.name = "TaskNotFoundError";
|
|
3468
3468
|
}
|
|
3469
3469
|
};
|
|
3470
|
+
TaskReferenceAmbiguousError = class TaskReferenceAmbiguousError extends Error {
|
|
3471
|
+
reference;
|
|
3472
|
+
static code = "TASK_REFERENCE_AMBIGUOUS";
|
|
3473
|
+
candidateTaskIds;
|
|
3474
|
+
candidateProjectIds;
|
|
3475
|
+
constructor(reference, candidates) {
|
|
3476
|
+
const candidateTaskIds = [...new Set(candidates.map((candidate) => candidate.task_id))].sort();
|
|
3477
|
+
const candidateProjectIds = [...new Set(candidates.map((candidate) => candidate.project_id).filter((projectId) => typeof projectId === "string" && projectId.length > 0))].sort();
|
|
3478
|
+
const projectList = candidateProjectIds.length > 0 ? candidateProjectIds.join(", ") : "(unscoped)";
|
|
3479
|
+
super(`Task reference is ambiguous: "${reference}". Candidate project IDs: ${projectList}. ` + "Use a full task UUID.");
|
|
3480
|
+
this.reference = reference;
|
|
3481
|
+
this.name = "TaskReferenceAmbiguousError";
|
|
3482
|
+
this.candidateTaskIds = candidateTaskIds;
|
|
3483
|
+
this.candidateProjectIds = candidateProjectIds;
|
|
3484
|
+
}
|
|
3485
|
+
};
|
|
3470
3486
|
ProjectNotFoundError = class ProjectNotFoundError extends Error {
|
|
3471
3487
|
projectId;
|
|
3472
3488
|
static code = "PROJECT_NOT_FOUND";
|
|
@@ -3758,15 +3774,18 @@ var init_identity_mapping = __esm(() => {
|
|
|
3758
3774
|
var exports_config = {};
|
|
3759
3775
|
__export(exports_config, {
|
|
3760
3776
|
parseStorageMode: () => parseStorageMode,
|
|
3777
|
+
parseStorageBackend: () => parseStorageBackend,
|
|
3761
3778
|
loadTodosStorageConfig: () => loadTodosStorageConfig,
|
|
3762
3779
|
loadStorageConfig: () => loadStorageConfig,
|
|
3763
3780
|
isTodosShadowEnabled: () => isTodosShadowEnabled,
|
|
3764
3781
|
isTodosRemoteStorageEnabled: () => isTodosRemoteStorageEnabled,
|
|
3782
|
+
isTodosPostgresBackend: () => isTodosPostgresBackend,
|
|
3765
3783
|
getTodosStorageShadowEnvName: () => getTodosStorageShadowEnvName,
|
|
3766
3784
|
getTodosStorageMode: () => getTodosStorageMode,
|
|
3767
3785
|
getTodosStorageEnvName: () => getTodosStorageEnvName,
|
|
3768
3786
|
getTodosStorageDatabaseUrl: () => getTodosStorageDatabaseUrl,
|
|
3769
3787
|
getTodosStorageDatabaseEnv: () => getTodosStorageDatabaseEnv,
|
|
3788
|
+
getTodosStorageBackend: () => getTodosStorageBackend,
|
|
3770
3789
|
getStorageMode: () => getStorageMode,
|
|
3771
3790
|
getStorageDatabaseUrl: () => getStorageDatabaseUrl,
|
|
3772
3791
|
getStorageDatabaseEnv: () => getStorageDatabaseEnv,
|
|
@@ -3791,7 +3810,7 @@ function getCanonicalTodosRdsConfig(env = process.env) {
|
|
|
3791
3810
|
};
|
|
3792
3811
|
}
|
|
3793
3812
|
function loadTodosStorageConfig(env = process.env) {
|
|
3794
|
-
const mode =
|
|
3813
|
+
const mode = getTodosStorageBackend(env);
|
|
3795
3814
|
const databaseUrl = getTodosStorageDatabaseUrl(env);
|
|
3796
3815
|
const bucket = readStorageEnv(env, "s3Bucket").value;
|
|
3797
3816
|
const prefix = readStorageEnv(env, "s3Prefix").value ?? "todos/";
|
|
@@ -3828,29 +3847,41 @@ function loadTodosStorageConfig(env = process.env) {
|
|
|
3828
3847
|
function loadStorageConfig(env = process.env) {
|
|
3829
3848
|
return loadTodosStorageConfig(env);
|
|
3830
3849
|
}
|
|
3850
|
+
function isTodosPostgresBackend(config) {
|
|
3851
|
+
return config.mode === "postgres";
|
|
3852
|
+
}
|
|
3831
3853
|
function isTodosRemoteStorageEnabled(config) {
|
|
3832
|
-
return config
|
|
3854
|
+
return isTodosPostgresBackend(config);
|
|
3833
3855
|
}
|
|
3834
3856
|
function assertTodosRemoteStorageConfig(config) {
|
|
3835
|
-
if (!
|
|
3857
|
+
if (!isTodosPostgresBackend(config))
|
|
3836
3858
|
return;
|
|
3837
3859
|
if (!config.database?.url) {
|
|
3838
3860
|
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${TODOS_STORAGE_ENV.mode}=${config.mode}`);
|
|
3839
3861
|
}
|
|
3840
3862
|
}
|
|
3841
|
-
function
|
|
3863
|
+
function parseStorageBackend(value) {
|
|
3842
3864
|
const normalized = clean(value)?.toLowerCase();
|
|
3843
3865
|
if (!normalized)
|
|
3844
|
-
return "
|
|
3845
|
-
if (normalized === "
|
|
3866
|
+
return "sqlite";
|
|
3867
|
+
if (normalized === "sqlite" || normalized === "postgres")
|
|
3846
3868
|
return normalized;
|
|
3847
|
-
|
|
3869
|
+
const legacy = LEGACY_BACKEND_TOKENS[normalized];
|
|
3870
|
+
if (legacy)
|
|
3871
|
+
return legacy;
|
|
3872
|
+
throw new Error(`${TODOS_STORAGE_ENV.mode} must be sqlite or postgres (legacy values local and remote are accepted)`);
|
|
3873
|
+
}
|
|
3874
|
+
function parseStorageMode(value) {
|
|
3875
|
+
return parseStorageBackend(value);
|
|
3876
|
+
}
|
|
3877
|
+
function getTodosStorageBackend(env = process.env) {
|
|
3878
|
+
return parseStorageBackend(readStorageEnv(env, "mode").value);
|
|
3848
3879
|
}
|
|
3849
3880
|
function getTodosStorageMode(env = process.env) {
|
|
3850
|
-
return
|
|
3881
|
+
return getTodosStorageBackend(env);
|
|
3851
3882
|
}
|
|
3852
3883
|
function getStorageMode(env = process.env) {
|
|
3853
|
-
return
|
|
3884
|
+
return getTodosStorageBackend(env);
|
|
3854
3885
|
}
|
|
3855
3886
|
function isTodosShadowEnabled(env = process.env) {
|
|
3856
3887
|
return parseBoolean(readStorageEnv(env, "shadow").value, false);
|
|
@@ -3861,8 +3892,8 @@ function getTodosStorageShadowEnvName(env = process.env) {
|
|
|
3861
3892
|
function assertTodosShadowConfig(config, env = process.env) {
|
|
3862
3893
|
if (!isTodosShadowEnabled(env))
|
|
3863
3894
|
return;
|
|
3864
|
-
if (config.mode !== "
|
|
3865
|
-
throw new Error(`${readStorageEnv(env, "shadow").name} shadow mirror requires
|
|
3895
|
+
if (config.mode !== "sqlite") {
|
|
3896
|
+
throw new Error(`${readStorageEnv(env, "shadow").name} shadow mirror requires the sqlite backend (got ${config.mode})`);
|
|
3866
3897
|
}
|
|
3867
3898
|
if (!config.database?.url) {
|
|
3868
3899
|
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${readStorageEnv(env, "shadow").name} is enabled`);
|
|
@@ -3918,7 +3949,7 @@ function parsePositiveInteger(value, fallback) {
|
|
|
3918
3949
|
}
|
|
3919
3950
|
return parsed;
|
|
3920
3951
|
}
|
|
3921
|
-
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";
|
|
3952
|
+
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;
|
|
3922
3953
|
var init_config = __esm(() => {
|
|
3923
3954
|
TODOS_STORAGE_TABLES = [
|
|
3924
3955
|
"todos_sync_records",
|
|
@@ -3959,6 +3990,14 @@ var init_config = __esm(() => {
|
|
|
3959
3990
|
syncBatchSize: "TODOS_SYNC_BATCH_SIZE",
|
|
3960
3991
|
syncDryRun: "TODOS_SYNC_DRY_RUN"
|
|
3961
3992
|
};
|
|
3993
|
+
LEGACY_BACKEND_TOKENS = {
|
|
3994
|
+
local: "sqlite",
|
|
3995
|
+
remote: "postgres",
|
|
3996
|
+
postgresql: "postgres",
|
|
3997
|
+
hybrid: "postgres",
|
|
3998
|
+
self_hosted: "postgres",
|
|
3999
|
+
cloud: "postgres"
|
|
4000
|
+
};
|
|
3962
4001
|
});
|
|
3963
4002
|
|
|
3964
4003
|
// src/storage/shadow-outbox-schema.ts
|
|
@@ -4092,6 +4131,30 @@ function findGitRoot(startDir) {
|
|
|
4092
4131
|
}
|
|
4093
4132
|
return null;
|
|
4094
4133
|
}
|
|
4134
|
+
function getGlobalDbPath() {
|
|
4135
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
4136
|
+
return join(home, ".hasna", "todos", "todos.db");
|
|
4137
|
+
}
|
|
4138
|
+
function hasExplicitProjectArg(args = process.argv.slice(2)) {
|
|
4139
|
+
return args.some((arg) => arg === "--project" || arg.startsWith("--project="));
|
|
4140
|
+
}
|
|
4141
|
+
function getCliCommand(args = process.argv.slice(2)) {
|
|
4142
|
+
const globalOptionsWithValues = new Set(["--project", "--agent", "--session"]);
|
|
4143
|
+
for (let index = 0;index < args.length; index += 1) {
|
|
4144
|
+
const arg = args[index];
|
|
4145
|
+
if (globalOptionsWithValues.has(arg)) {
|
|
4146
|
+
index += 1;
|
|
4147
|
+
continue;
|
|
4148
|
+
}
|
|
4149
|
+
if (arg.startsWith("-"))
|
|
4150
|
+
continue;
|
|
4151
|
+
return arg;
|
|
4152
|
+
}
|
|
4153
|
+
return null;
|
|
4154
|
+
}
|
|
4155
|
+
function canCreateScopedProjectDb(args = process.argv.slice(2)) {
|
|
4156
|
+
return getCliCommand(args) === "project-bootstrap" && !args.some((arg) => arg === "--dry-run" || arg.startsWith("--dry-run="));
|
|
4157
|
+
}
|
|
4095
4158
|
function getDbPath() {
|
|
4096
4159
|
if (process.env["HASNA_TODOS_DB_PATH"]) {
|
|
4097
4160
|
return process.env["HASNA_TODOS_DB_PATH"];
|
|
@@ -4099,18 +4162,19 @@ function getDbPath() {
|
|
|
4099
4162
|
if (process.env["TODOS_DB_PATH"]) {
|
|
4100
4163
|
return process.env["TODOS_DB_PATH"];
|
|
4101
4164
|
}
|
|
4165
|
+
if (hasExplicitProjectArg())
|
|
4166
|
+
return getGlobalDbPath();
|
|
4102
4167
|
const cwd = process.cwd();
|
|
4103
4168
|
const nearest = findNearestProjectDb(cwd);
|
|
4104
4169
|
if (nearest)
|
|
4105
4170
|
return nearest;
|
|
4106
4171
|
if (process.env["TODOS_DB_SCOPE"] === "project") {
|
|
4107
4172
|
const gitRoot = findGitRoot(cwd);
|
|
4108
|
-
if (gitRoot) {
|
|
4173
|
+
if (gitRoot && canCreateScopedProjectDb()) {
|
|
4109
4174
|
return join(gitRoot, ".hasna", "todos", "todos.db");
|
|
4110
4175
|
}
|
|
4111
4176
|
}
|
|
4112
|
-
|
|
4113
|
-
return join(home, ".hasna", "todos", "todos.db");
|
|
4177
|
+
return getGlobalDbPath();
|
|
4114
4178
|
}
|
|
4115
4179
|
function getDatabasePath() {
|
|
4116
4180
|
return getDbPath();
|
|
@@ -4213,10 +4277,13 @@ function resolvePartialId(db, table, partialId) {
|
|
|
4213
4277
|
return null;
|
|
4214
4278
|
}
|
|
4215
4279
|
if (table === "tasks") {
|
|
4216
|
-
const shortIdRows = db.query("SELECT id FROM tasks WHERE short_id = ?").all(partialId);
|
|
4280
|
+
const shortIdRows = db.query("SELECT id, project_id FROM tasks WHERE LOWER(short_id) = LOWER(?) ORDER BY project_id, id").all(partialId);
|
|
4217
4281
|
if (shortIdRows.length === 1) {
|
|
4218
4282
|
return shortIdRows[0].id;
|
|
4219
4283
|
}
|
|
4284
|
+
if (shortIdRows.length > 1) {
|
|
4285
|
+
throw new TaskReferenceAmbiguousError(partialId, shortIdRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
|
|
4286
|
+
}
|
|
4220
4287
|
}
|
|
4221
4288
|
if (table === "task_lists") {
|
|
4222
4289
|
const slugRow = db.query("SELECT id FROM task_lists WHERE slug = ?").get(partialId);
|
|
@@ -11946,7 +12013,7 @@ var init_dispatches = __esm(() => {
|
|
|
11946
12013
|
// package.json
|
|
11947
12014
|
var package_default = {
|
|
11948
12015
|
name: "@hasna/todos",
|
|
11949
|
-
version: "0.13.
|
|
12016
|
+
version: "0.13.3",
|
|
11950
12017
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
11951
12018
|
type: "module",
|
|
11952
12019
|
main: "dist/index.js",
|
|
@@ -11980,6 +12047,10 @@ var package_default = {
|
|
|
11980
12047
|
"./storage": {
|
|
11981
12048
|
types: "./dist/storage.d.ts",
|
|
11982
12049
|
import: "./dist/storage.js"
|
|
12050
|
+
},
|
|
12051
|
+
"./testing": {
|
|
12052
|
+
types: "./dist/testing.d.ts",
|
|
12053
|
+
import: "./dist/testing.js"
|
|
11983
12054
|
}
|
|
11984
12055
|
},
|
|
11985
12056
|
workspaces: [
|
|
@@ -11992,8 +12063,8 @@ var package_default = {
|
|
|
11992
12063
|
"README.md"
|
|
11993
12064
|
],
|
|
11994
12065
|
scripts: {
|
|
11995
|
-
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",
|
|
11996
|
-
"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/*'",
|
|
12066
|
+
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",
|
|
12067
|
+
"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/*'",
|
|
11997
12068
|
migrate: "bun run src/server/index.ts migrate",
|
|
11998
12069
|
"backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
|
|
11999
12070
|
"generate:sdk": "bun run scripts/generate-sdk.ts",
|
|
@@ -17740,11 +17811,12 @@ var DEFAULT_LIMITS = {
|
|
|
17740
17811
|
verification_limit: 10,
|
|
17741
17812
|
run_limit: 3,
|
|
17742
17813
|
dependency_limit: 12,
|
|
17743
|
-
plan_task_limit:
|
|
17814
|
+
plan_task_limit: 24,
|
|
17744
17815
|
max_text_chars: 6000,
|
|
17745
17816
|
summary_char_limit: 480,
|
|
17746
17817
|
stale_after_hours: 72
|
|
17747
17818
|
};
|
|
17819
|
+
var MAX_CONTEXT_TASKS = 24;
|
|
17748
17820
|
var ALL_CONTEXT_SECTIONS = [
|
|
17749
17821
|
"project",
|
|
17750
17822
|
"plan",
|
|
@@ -17777,7 +17849,7 @@ function limits(input) {
|
|
|
17777
17849
|
verification_limit: clamp(input.verification_limit, DEFAULT_LIMITS.verification_limit, 100),
|
|
17778
17850
|
run_limit: clamp(input.run_limit, DEFAULT_LIMITS.run_limit, 20),
|
|
17779
17851
|
dependency_limit: clamp(input.dependency_limit, DEFAULT_LIMITS.dependency_limit, 100),
|
|
17780
|
-
plan_task_limit: clamp(input.plan_task_limit, DEFAULT_LIMITS.plan_task_limit,
|
|
17852
|
+
plan_task_limit: clamp(input.plan_task_limit, DEFAULT_LIMITS.plan_task_limit, MAX_CONTEXT_TASKS),
|
|
17781
17853
|
max_text_chars: clamp(input.max_text_chars, DEFAULT_LIMITS.max_text_chars, 50000),
|
|
17782
17854
|
summary_char_limit: clamp(input.summary_char_limit, DEFAULT_LIMITS.summary_char_limit, 4000),
|
|
17783
17855
|
stale_after_hours: clamp(input.stale_after_hours, DEFAULT_LIMITS.stale_after_hours, 24 * 365)
|
|
@@ -17800,6 +17872,13 @@ function taskSummary(task2) {
|
|
|
17800
17872
|
priority: task2.priority
|
|
17801
17873
|
};
|
|
17802
17874
|
}
|
|
17875
|
+
function latestPlanTasks(planId, limit, db) {
|
|
17876
|
+
const rows = db.query(`SELECT * FROM tasks
|
|
17877
|
+
WHERE plan_id = ? AND archived_at IS NULL
|
|
17878
|
+
ORDER BY updated_at DESC, created_at DESC, id ASC
|
|
17879
|
+
LIMIT ?`).all(planId, limit);
|
|
17880
|
+
return rows.map(rowToTask);
|
|
17881
|
+
}
|
|
17803
17882
|
function acceptanceCriteria(task2, maxText) {
|
|
17804
17883
|
const metadata = task2.metadata || {};
|
|
17805
17884
|
const raw = metadata["acceptance_criteria"] ?? metadata["acceptanceCriteria"] ?? metadata["criteria"];
|
|
@@ -17897,7 +17976,7 @@ function summarizeSection(pack, section, maxChars) {
|
|
|
17897
17976
|
return "No plan was attached.";
|
|
17898
17977
|
return summarizeStrings([
|
|
17899
17978
|
`Plan ${pack.plan.name} is ${pack.plan.status}`,
|
|
17900
|
-
`${pack.plan.tasks.length} listed plan
|
|
17979
|
+
`${pack.plan.tasks.length} of ${pack.plan.total_tasks} plan tasks listed; ${pack.plan.omitted_tasks} omitted`,
|
|
17901
17980
|
...pack.plan.tasks.slice(0, 4).map((task2) => `${task2.status} ${task2.short_id || task2.id.slice(0, 8)} ${task2.title}`)
|
|
17902
17981
|
], maxChars);
|
|
17903
17982
|
}
|
|
@@ -18047,7 +18126,8 @@ function createAgentContextPack(input, db) {
|
|
|
18047
18126
|
const limit = limits(input);
|
|
18048
18127
|
const project = task2.project_id ? getProject(task2.project_id, d) : null;
|
|
18049
18128
|
const plan = task2.plan_id ? getPlan(task2.plan_id, d) : null;
|
|
18050
|
-
const planTasks = task2.plan_id ?
|
|
18129
|
+
const planTasks = task2.plan_id ? latestPlanTasks(task2.plan_id, limit.plan_task_limit, d) : [];
|
|
18130
|
+
const totalPlanTasks = task2.plan_id ? countTasks({ plan_id: task2.plan_id }, d) : 0;
|
|
18051
18131
|
const upstream = getTaskDependencies(task2.id, d).map((dep) => taskSummary(getTask(dep.depends_on, d))).filter((item) => Boolean(item));
|
|
18052
18132
|
const downstream = getTaskDependents(task2.id, d).map((dep) => taskSummary(getTask(dep.task_id, d))).filter((item) => Boolean(item));
|
|
18053
18133
|
const comments = listComments(task2.id, d);
|
|
@@ -18090,6 +18170,9 @@ function createAgentContextPack(input, db) {
|
|
|
18090
18170
|
warnings.push(`${runs.length - selectedRuns.length} older runs omitted`);
|
|
18091
18171
|
if (fileMap.size > relevantFiles.length)
|
|
18092
18172
|
warnings.push(`${fileMap.size - relevantFiles.length} relevant files omitted`);
|
|
18173
|
+
if (totalPlanTasks > planTasks.length && plan) {
|
|
18174
|
+
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>"`);
|
|
18175
|
+
}
|
|
18093
18176
|
const contextTask = {
|
|
18094
18177
|
id: task2.id,
|
|
18095
18178
|
short_id: task2.short_id,
|
|
@@ -18121,8 +18204,9 @@ function createAgentContextPack(input, db) {
|
|
|
18121
18204
|
description: truncate2(plan.description, limit.max_text_chars),
|
|
18122
18205
|
status: plan.status,
|
|
18123
18206
|
agent_id: plan.agent_id,
|
|
18124
|
-
tasks: planTasks.
|
|
18125
|
-
|
|
18207
|
+
tasks: planTasks.map(taskSummary).filter((item) => Boolean(item)),
|
|
18208
|
+
total_tasks: totalPlanTasks,
|
|
18209
|
+
omitted_tasks: Math.max(0, totalPlanTasks - planTasks.length)
|
|
18126
18210
|
} : null,
|
|
18127
18211
|
acceptance_criteria: acceptanceCriteria(task2, limit.max_text_chars),
|
|
18128
18212
|
dependencies: {
|
|
@@ -18232,6 +18316,8 @@ ${pack.task.description}` : null,
|
|
|
18232
18316
|
"## Project And Plan",
|
|
18233
18317
|
pack.project ? `- Project: ${pack.project.name} (${pack.project.path})` : "- Project: none",
|
|
18234
18318
|
pack.plan ? `- Plan: ${pack.plan.name} (${pack.plan.status})` : "- Plan: none",
|
|
18319
|
+
pack.plan ? `- Plan tasks: ${pack.plan.tasks.length} shown of ${pack.plan.total_tasks} total (${pack.plan.omitted_tasks} omitted)` : null,
|
|
18320
|
+
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,
|
|
18235
18321
|
pack.plan && pack.plan.tasks.length > 0 ? bullet(pack.plan.tasks.map((task2) => `${task2.status} ${task2.short_id || task2.id.slice(0, 8)} ${task2.title}`)) : null,
|
|
18236
18322
|
"",
|
|
18237
18323
|
"## Acceptance Criteria",
|
|
@@ -19900,7 +19986,7 @@ function createReleaseCompatibilityReport(options = {}) {
|
|
|
19900
19986
|
"todos --help",
|
|
19901
19987
|
"todos-mcp --help",
|
|
19902
19988
|
"todos-serve --help",
|
|
19903
|
-
"todos doctor"
|
|
19989
|
+
"todos doctor --no-fail-on-findings"
|
|
19904
19990
|
],
|
|
19905
19991
|
smoke_tests: [
|
|
19906
19992
|
"command -v todos",
|
|
@@ -19908,7 +19994,7 @@ function createReleaseCompatibilityReport(options = {}) {
|
|
|
19908
19994
|
"command -v todos-serve",
|
|
19909
19995
|
"todos --version",
|
|
19910
19996
|
"todos --help",
|
|
19911
|
-
"todos doctor"
|
|
19997
|
+
"todos doctor --no-fail-on-findings"
|
|
19912
19998
|
],
|
|
19913
19999
|
rollback: [
|
|
19914
20000
|
"npm view @hasna/todos versions --json",
|
|
@@ -20402,12 +20488,11 @@ ${task2.description || ""}`.split(/\r?\n/);
|
|
|
20402
20488
|
function normalizeStackLocation(value) {
|
|
20403
20489
|
return value.replace(/:\d+(?::\d+)?/g, "").replace(/^file:\/\//, "").toLowerCase().trim();
|
|
20404
20490
|
}
|
|
20405
|
-
function
|
|
20491
|
+
function fingerprintContent(task2) {
|
|
20406
20492
|
const body = task2.description || "";
|
|
20407
20493
|
const text = `${task2.title}
|
|
20408
20494
|
${body}`;
|
|
20409
20495
|
return {
|
|
20410
|
-
task: task2,
|
|
20411
20496
|
title: normalizeText(task2.title),
|
|
20412
20497
|
body: normalizeText(body),
|
|
20413
20498
|
text: normalizeText(text),
|
|
@@ -20416,6 +20501,9 @@ ${body}`;
|
|
|
20416
20501
|
stackKeys: stackKeysFor(task2)
|
|
20417
20502
|
};
|
|
20418
20503
|
}
|
|
20504
|
+
function fingerprint(task2) {
|
|
20505
|
+
return { task: task2, ...fingerprintContent(task2) };
|
|
20506
|
+
}
|
|
20419
20507
|
function intersects(left, right) {
|
|
20420
20508
|
for (const value of left)
|
|
20421
20509
|
if (right.has(value))
|
|
@@ -23602,6 +23690,13 @@ var TODOS_PACKAGE_EXPORTS = [
|
|
|
23602
23690
|
types: "./dist/storage.d.ts",
|
|
23603
23691
|
description: "Storage and service adapter contracts for local SQLite implementations.",
|
|
23604
23692
|
stability: "stable"
|
|
23693
|
+
},
|
|
23694
|
+
{
|
|
23695
|
+
subpath: "./testing",
|
|
23696
|
+
import: "./dist/testing.js",
|
|
23697
|
+
types: "./dist/testing.d.ts",
|
|
23698
|
+
description: "Test-store isolation helpers that keep a consumer's test suite off a shared todos store.",
|
|
23699
|
+
stability: "stable"
|
|
23605
23700
|
}
|
|
23606
23701
|
];
|
|
23607
23702
|
function source8(version) {
|
|
@@ -23633,6 +23728,9 @@ var TODOS_REGISTRY = createTodosRegistry({
|
|
|
23633
23728
|
// src/storage/index.ts
|
|
23634
23729
|
init_config();
|
|
23635
23730
|
|
|
23731
|
+
// src/storage/local-sqlite.ts
|
|
23732
|
+
init_types();
|
|
23733
|
+
|
|
23636
23734
|
// src/lib/search.ts
|
|
23637
23735
|
init_database();
|
|
23638
23736
|
function rowToTask3(row) {
|
|
@@ -24449,6 +24547,267 @@ init_audit();
|
|
|
24449
24547
|
init_comments();
|
|
24450
24548
|
init_database();
|
|
24451
24549
|
|
|
24550
|
+
// src/db/integrity.ts
|
|
24551
|
+
init_database();
|
|
24552
|
+
|
|
24553
|
+
// src/lib/integrity.ts
|
|
24554
|
+
init_types();
|
|
24555
|
+
var TODOS_INTEGRITY_SCHEMA_VERSION = "todos.integrity.v1";
|
|
24556
|
+
var TERMINAL_TASK_STATUSES = ["completed", "failed", "cancelled"];
|
|
24557
|
+
var OPEN_TASK_STATUSES = TASK_STATUSES.filter((status) => !TERMINAL_TASK_STATUSES.includes(status));
|
|
24558
|
+
var INTEGRITY_CONDITIONS = [
|
|
24559
|
+
{
|
|
24560
|
+
id: "tasks_without_project",
|
|
24561
|
+
entity: "task",
|
|
24562
|
+
field: "project_id",
|
|
24563
|
+
target: "project",
|
|
24564
|
+
kind: "missing",
|
|
24565
|
+
base_severity: "warn",
|
|
24566
|
+
escalate_when_open: true,
|
|
24567
|
+
impact: "invisible to every project-scoped read (list, status, next, claim)"
|
|
24568
|
+
},
|
|
24569
|
+
{
|
|
24570
|
+
id: "tasks_without_task_list",
|
|
24571
|
+
entity: "task",
|
|
24572
|
+
field: "task_list_id",
|
|
24573
|
+
target: "task_list",
|
|
24574
|
+
kind: "missing",
|
|
24575
|
+
base_severity: "warn",
|
|
24576
|
+
escalate_when_open: true,
|
|
24577
|
+
impact: "invisible to every task-list read \u2014 the list reports zero open work"
|
|
24578
|
+
},
|
|
24579
|
+
{
|
|
24580
|
+
id: "tasks_with_unregistered_project",
|
|
24581
|
+
entity: "task",
|
|
24582
|
+
field: "project_id",
|
|
24583
|
+
target: "project",
|
|
24584
|
+
kind: "dangling",
|
|
24585
|
+
base_severity: "error",
|
|
24586
|
+
escalate_when_open: false,
|
|
24587
|
+
impact: "points at a project id that does not exist; the reference can never resolve"
|
|
24588
|
+
},
|
|
24589
|
+
{
|
|
24590
|
+
id: "tasks_with_unregistered_task_list",
|
|
24591
|
+
entity: "task",
|
|
24592
|
+
field: "task_list_id",
|
|
24593
|
+
target: "task_list",
|
|
24594
|
+
kind: "dangling",
|
|
24595
|
+
base_severity: "error",
|
|
24596
|
+
escalate_when_open: false,
|
|
24597
|
+
impact: "points at a task-list id that does not exist; the reference can never resolve"
|
|
24598
|
+
},
|
|
24599
|
+
{
|
|
24600
|
+
id: "task_lists_without_project",
|
|
24601
|
+
entity: "task_list",
|
|
24602
|
+
field: "project_id",
|
|
24603
|
+
target: "project",
|
|
24604
|
+
kind: "missing",
|
|
24605
|
+
base_severity: "warn",
|
|
24606
|
+
escalate_when_open: false,
|
|
24607
|
+
impact: "unbound list \u2014 unreachable from any project, so its tasks are unroutable"
|
|
24608
|
+
},
|
|
24609
|
+
{
|
|
24610
|
+
id: "task_lists_with_unregistered_project",
|
|
24611
|
+
entity: "task_list",
|
|
24612
|
+
field: "project_id",
|
|
24613
|
+
target: "project",
|
|
24614
|
+
kind: "dangling",
|
|
24615
|
+
base_severity: "error",
|
|
24616
|
+
escalate_when_open: false,
|
|
24617
|
+
impact: "points at a project id that does not exist; the list can never be reached"
|
|
24618
|
+
}
|
|
24619
|
+
];
|
|
24620
|
+
function integrityCondition(id) {
|
|
24621
|
+
const spec = INTEGRITY_CONDITIONS.find((condition) => condition.id === id);
|
|
24622
|
+
if (!spec)
|
|
24623
|
+
throw new Error(`unknown integrity condition: ${id}`);
|
|
24624
|
+
return spec;
|
|
24625
|
+
}
|
|
24626
|
+
function resolveIntegritySeverity(spec, measurement) {
|
|
24627
|
+
if (spec.kind === "dangling")
|
|
24628
|
+
return "error";
|
|
24629
|
+
if (spec.escalate_when_open && (measurement.open_count ?? 0) > 0)
|
|
24630
|
+
return "error";
|
|
24631
|
+
return spec.base_severity;
|
|
24632
|
+
}
|
|
24633
|
+
var ENTITY_LABEL = {
|
|
24634
|
+
task: ["task", "tasks"],
|
|
24635
|
+
task_list: ["task list", "task lists"]
|
|
24636
|
+
};
|
|
24637
|
+
function plural(entity, count2) {
|
|
24638
|
+
const [one, many] = ENTITY_LABEL[entity];
|
|
24639
|
+
return count2 === 1 ? one : many;
|
|
24640
|
+
}
|
|
24641
|
+
function formatIntegrityMessage(spec, measurement) {
|
|
24642
|
+
const noun = plural(spec.entity, measurement.count);
|
|
24643
|
+
const verb = measurement.count === 1 ? ["has", "references"] : ["have", "reference"];
|
|
24644
|
+
const open = measurement.open_count === null || measurement.open_count === 0 ? "" : ` (${measurement.open_count} still open)`;
|
|
24645
|
+
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}`;
|
|
24646
|
+
}
|
|
24647
|
+
function measuredCondition(spec, measurement, source9) {
|
|
24648
|
+
return {
|
|
24649
|
+
id: spec.id,
|
|
24650
|
+
entity: spec.entity,
|
|
24651
|
+
field: spec.field,
|
|
24652
|
+
kind: spec.kind,
|
|
24653
|
+
count: measurement.count,
|
|
24654
|
+
open_count: measurement.open_count,
|
|
24655
|
+
severity: measurement.count > 0 ? resolveIntegritySeverity(spec, measurement) : null,
|
|
24656
|
+
verified: true,
|
|
24657
|
+
source: source9,
|
|
24658
|
+
message: formatIntegrityMessage(spec, measurement),
|
|
24659
|
+
impact: spec.impact
|
|
24660
|
+
};
|
|
24661
|
+
}
|
|
24662
|
+
function unverifiedCondition(spec, reason) {
|
|
24663
|
+
return {
|
|
24664
|
+
id: spec.id,
|
|
24665
|
+
entity: spec.entity,
|
|
24666
|
+
field: spec.field,
|
|
24667
|
+
kind: spec.kind,
|
|
24668
|
+
count: null,
|
|
24669
|
+
open_count: null,
|
|
24670
|
+
severity: null,
|
|
24671
|
+
verified: false,
|
|
24672
|
+
source: "unverified",
|
|
24673
|
+
unverified_reason: reason,
|
|
24674
|
+
message: `${spec.id}: NOT CHECKED \u2014 ${reason}`,
|
|
24675
|
+
impact: spec.impact
|
|
24676
|
+
};
|
|
24677
|
+
}
|
|
24678
|
+
function summarizeIntegrity(conditions) {
|
|
24679
|
+
const measured = conditions.filter((condition) => condition.verified && condition.count !== null);
|
|
24680
|
+
const findings = measured.filter((condition) => (condition.count ?? 0) > 0);
|
|
24681
|
+
const unverified = conditions.length - measured.length;
|
|
24682
|
+
return {
|
|
24683
|
+
ok: unverified === 0 && findings.length === 0,
|
|
24684
|
+
findings: findings.length,
|
|
24685
|
+
rows: findings.reduce((total, condition) => total + (condition.count ?? 0), 0),
|
|
24686
|
+
errors: findings.filter((condition) => condition.severity === "error").length,
|
|
24687
|
+
warnings: findings.filter((condition) => condition.severity === "warn").length,
|
|
24688
|
+
unverified,
|
|
24689
|
+
complete: unverified === 0
|
|
24690
|
+
};
|
|
24691
|
+
}
|
|
24692
|
+
function buildIntegrityReport(conditions, generatedAt) {
|
|
24693
|
+
const measuredSources = [...new Set(conditions.map((condition) => condition.source))].filter((source9) => source9 !== "unverified");
|
|
24694
|
+
return {
|
|
24695
|
+
schema_version: TODOS_INTEGRITY_SCHEMA_VERSION,
|
|
24696
|
+
generated_at: generatedAt,
|
|
24697
|
+
source: measuredSources.length === 0 ? "unverified" : measuredSources.length === 1 ? measuredSources[0] : "remote-derived",
|
|
24698
|
+
conditions,
|
|
24699
|
+
summary: summarizeIntegrity(conditions)
|
|
24700
|
+
};
|
|
24701
|
+
}
|
|
24702
|
+
function adoptRemoteIntegrityReport(raw, fallbackGeneratedAt) {
|
|
24703
|
+
const received = new Map;
|
|
24704
|
+
if (Array.isArray(raw.conditions)) {
|
|
24705
|
+
for (const entry of raw.conditions) {
|
|
24706
|
+
if (entry && typeof entry["id"] === "string")
|
|
24707
|
+
received.set(entry["id"], entry);
|
|
24708
|
+
}
|
|
24709
|
+
}
|
|
24710
|
+
const conditions = INTEGRITY_CONDITIONS.map((spec) => {
|
|
24711
|
+
const entry = received.get(spec.id);
|
|
24712
|
+
const count2 = entry?.["count"];
|
|
24713
|
+
const verified = entry?.["verified"];
|
|
24714
|
+
if (!entry || verified === false || typeof count2 !== "number" || !Number.isFinite(count2)) {
|
|
24715
|
+
return unverifiedCondition(spec, entry ? typeof entry["unverified_reason"] === "string" ? String(entry["unverified_reason"]) : "authority reported this condition without a usable count" : "authority did not report this condition");
|
|
24716
|
+
}
|
|
24717
|
+
const openRaw = entry["open_count"];
|
|
24718
|
+
return measuredCondition(spec, {
|
|
24719
|
+
count: count2,
|
|
24720
|
+
open_count: spec.entity === "task" ? typeof openRaw === "number" && Number.isFinite(openRaw) ? openRaw : 0 : null
|
|
24721
|
+
}, typeof raw.source === "string" && raw.source === "postgres" ? "postgres" : "remote-authority");
|
|
24722
|
+
});
|
|
24723
|
+
const report = buildIntegrityReport(conditions, typeof raw.generated_at === "string" ? raw.generated_at : fallbackGeneratedAt);
|
|
24724
|
+
return typeof raw.source === "string" && (raw.source === "sqlite" || raw.source === "postgres") ? { ...report, source: raw.source } : report;
|
|
24725
|
+
}
|
|
24726
|
+
var SQLITE_TABLE = {
|
|
24727
|
+
task: "tasks",
|
|
24728
|
+
task_list: "task_lists",
|
|
24729
|
+
project: "projects"
|
|
24730
|
+
};
|
|
24731
|
+
function sqliteOpenStatusList() {
|
|
24732
|
+
return OPEN_TASK_STATUSES.map((status) => `'${status}'`).join(", ");
|
|
24733
|
+
}
|
|
24734
|
+
function buildSqliteIntegritySql(spec) {
|
|
24735
|
+
const table = SQLITE_TABLE[spec.entity];
|
|
24736
|
+
const target = SQLITE_TABLE[spec.target];
|
|
24737
|
+
const column = `t."${spec.field}"`;
|
|
24738
|
+
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})`;
|
|
24739
|
+
const openExpr = spec.entity === "task" ? `SUM(CASE WHEN t."status" IN (${sqliteOpenStatusList()}) THEN 1 ELSE 0 END)` : "NULL";
|
|
24740
|
+
return `SELECT COUNT(*) AS count, ${openExpr} AS open_count FROM "${table}" t WHERE ${predicate}`;
|
|
24741
|
+
}
|
|
24742
|
+
var RECORD_TYPE = {
|
|
24743
|
+
task: "tasks",
|
|
24744
|
+
task_list: "task_lists",
|
|
24745
|
+
project: "projects"
|
|
24746
|
+
};
|
|
24747
|
+
function buildPostgresIntegritySql(spec, options) {
|
|
24748
|
+
const params = [options.service, RECORD_TYPE[spec.entity]];
|
|
24749
|
+
const p = (value) => {
|
|
24750
|
+
params.push(value);
|
|
24751
|
+
return `$${params.length}`;
|
|
24752
|
+
};
|
|
24753
|
+
const column = `t.payload->>'${spec.field}'`;
|
|
24754
|
+
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})`;
|
|
24755
|
+
const openExpr = spec.entity === "task" ? `COUNT(*) FILTER (WHERE t.payload->>'status' IN (${OPEN_TASK_STATUSES.map((status) => p(status)).join(", ")}))::int` : "NULL::int";
|
|
24756
|
+
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}`;
|
|
24757
|
+
return { sql, params };
|
|
24758
|
+
}
|
|
24759
|
+
function referenceOf(row, field2) {
|
|
24760
|
+
const raw = row[field2];
|
|
24761
|
+
if (typeof raw !== "string")
|
|
24762
|
+
return null;
|
|
24763
|
+
const trimmed = raw.trim();
|
|
24764
|
+
return trimmed === "" ? null : trimmed;
|
|
24765
|
+
}
|
|
24766
|
+
function measureIntegrityRows(spec, sets) {
|
|
24767
|
+
const rows = spec.entity === "task" ? sets.tasks : sets.taskLists;
|
|
24768
|
+
if (!rows)
|
|
24769
|
+
return null;
|
|
24770
|
+
const registered = spec.kind === "dangling" ? spec.target === "project" ? sets.projectIds : sets.taskListIds : undefined;
|
|
24771
|
+
if (spec.kind === "dangling" && !registered)
|
|
24772
|
+
return null;
|
|
24773
|
+
let count2 = 0;
|
|
24774
|
+
let open = 0;
|
|
24775
|
+
for (const row of rows) {
|
|
24776
|
+
const reference = referenceOf(row, spec.field);
|
|
24777
|
+
const matches = spec.kind === "missing" ? reference === null : reference !== null && !registered.has(reference);
|
|
24778
|
+
if (!matches)
|
|
24779
|
+
continue;
|
|
24780
|
+
count2++;
|
|
24781
|
+
if (spec.entity === "task") {
|
|
24782
|
+
const status = row.status;
|
|
24783
|
+
if (typeof status === "string" && OPEN_TASK_STATUSES.includes(status))
|
|
24784
|
+
open++;
|
|
24785
|
+
}
|
|
24786
|
+
}
|
|
24787
|
+
return { count: count2, open_count: spec.entity === "task" ? open : null };
|
|
24788
|
+
}
|
|
24789
|
+
|
|
24790
|
+
// src/db/integrity.ts
|
|
24791
|
+
var REQUIRED_TABLES2 = { tasks: true, task_lists: true, projects: true };
|
|
24792
|
+
function tableExists(db, table) {
|
|
24793
|
+
return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
|
|
24794
|
+
}
|
|
24795
|
+
function scanSqliteIntegrity(db = getDatabase()) {
|
|
24796
|
+
const missing = Object.keys(REQUIRED_TABLES2).filter((table) => !tableExists(db, table));
|
|
24797
|
+
const conditions = INTEGRITY_CONDITIONS.map((spec) => {
|
|
24798
|
+
if (missing.length > 0) {
|
|
24799
|
+
return unverifiedCondition(spec, `local schema is missing table(s): ${missing.join(", ")}`);
|
|
24800
|
+
}
|
|
24801
|
+
try {
|
|
24802
|
+
const row = db.query(buildSqliteIntegritySql(spec)).get();
|
|
24803
|
+
return measuredCondition(spec, { count: Number(row?.count ?? 0), open_count: spec.entity === "task" ? Number(row?.open_count ?? 0) : null }, "sqlite");
|
|
24804
|
+
} catch (error) {
|
|
24805
|
+
return unverifiedCondition(spec, error instanceof Error ? error.message : String(error));
|
|
24806
|
+
}
|
|
24807
|
+
});
|
|
24808
|
+
return buildIntegrityReport(conditions, now());
|
|
24809
|
+
}
|
|
24810
|
+
|
|
24452
24811
|
// src/storage/sqlite-snapshot.ts
|
|
24453
24812
|
init_database();
|
|
24454
24813
|
init_audit();
|
|
@@ -24840,15 +25199,15 @@ function resolveTaskRefLocal(db, ref) {
|
|
|
24840
25199
|
return null;
|
|
24841
25200
|
if (TASK_UUID_RE.test(raw))
|
|
24842
25201
|
return getTask(raw, db);
|
|
24843
|
-
const prefixRows = db.query("SELECT id FROM tasks WHERE LOWER(id) LIKE ? ESCAPE '\\' LIMIT 2").all(`${raw.replace(/[\\%_]/g, (c) => `\\${c}`)}%`);
|
|
25202
|
+
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}`)}%`);
|
|
24844
25203
|
if (prefixRows.length > 1) {
|
|
24845
|
-
throw new
|
|
25204
|
+
throw new TaskReferenceAmbiguousError(ref, prefixRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
|
|
24846
25205
|
}
|
|
24847
25206
|
if (prefixRows.length === 1)
|
|
24848
25207
|
return getTask(prefixRows[0].id, db);
|
|
24849
|
-
const shortIdRows = db.query("SELECT id FROM tasks WHERE LOWER(short_id) = ? LIMIT 2").all(raw);
|
|
25208
|
+
const shortIdRows = db.query("SELECT id, project_id FROM tasks WHERE LOWER(short_id) = ? ORDER BY project_id, id LIMIT 2").all(raw);
|
|
24850
25209
|
if (shortIdRows.length > 1) {
|
|
24851
|
-
throw new
|
|
25210
|
+
throw new TaskReferenceAmbiguousError(ref, shortIdRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
|
|
24852
25211
|
}
|
|
24853
25212
|
if (shortIdRows.length === 1)
|
|
24854
25213
|
return getTask(shortIdRows[0].id, db);
|
|
@@ -24997,6 +25356,9 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
24997
25356
|
exportSnapshot: () => exportSqliteTodosStorageSnapshot(database()),
|
|
24998
25357
|
importSnapshot: (snapshot) => importSqliteTodosStorageSnapshot(snapshot, database())
|
|
24999
25358
|
},
|
|
25359
|
+
integrity: {
|
|
25360
|
+
report: () => scanSqliteIntegrity(database())
|
|
25361
|
+
},
|
|
25000
25362
|
transaction: (fn) => {
|
|
25001
25363
|
const tx = database().transaction(() => fn(adapter));
|
|
25002
25364
|
return tx();
|
|
@@ -25005,6 +25367,10 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
25005
25367
|
return adapter;
|
|
25006
25368
|
}
|
|
25007
25369
|
|
|
25370
|
+
// src/storage/postgres-adapter.ts
|
|
25371
|
+
init_types();
|
|
25372
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
25373
|
+
|
|
25008
25374
|
// src/storage/postgres-sync.ts
|
|
25009
25375
|
var DEFAULT_TODOS_POSTGRES_SYNC_TABLE = "todos_sync_records";
|
|
25010
25376
|
var DEFAULT_TODOS_POSTGRES_CURSOR_TABLE = "todos_sync_cursors";
|
|
@@ -25394,54 +25760,7 @@ function assertSafeIdentifier(value) {
|
|
|
25394
25760
|
throw new Error(`Unsafe Postgres identifier: ${value}`);
|
|
25395
25761
|
}
|
|
25396
25762
|
|
|
25397
|
-
// src/storage/hybrid.ts
|
|
25398
|
-
function createHybridTodosStorageAdapter(options) {
|
|
25399
|
-
const local = options.localAdapter ?? createLocalSqliteTodosStorageAdapter(options.local);
|
|
25400
|
-
const syncStore = options.syncStore ?? (options.postgresClient ? createPostgresTodosSyncStore(options.postgresClient, { sourceMachineId: options.sourceMachineId }) : null);
|
|
25401
|
-
if (!syncStore)
|
|
25402
|
-
throw new Error("hybrid storage requires a Postgres sync store or query client");
|
|
25403
|
-
if (!local.sync.exportSnapshot || !local.sync.importSnapshot) {
|
|
25404
|
-
throw new Error("hybrid storage requires local snapshot export/import support");
|
|
25405
|
-
}
|
|
25406
|
-
const exportSnapshot = async (context) => await local.sync.exportSnapshot(context);
|
|
25407
|
-
const importSnapshot = async (snapshot, context) => await local.sync.importSnapshot(snapshot, context);
|
|
25408
|
-
const remote = {
|
|
25409
|
-
ensureSchema: () => syncStore.ensureSchema(),
|
|
25410
|
-
async pushSnapshot(context) {
|
|
25411
|
-
const snapshot = await exportSnapshot(context);
|
|
25412
|
-
return syncStore.pushSnapshot(snapshot, context);
|
|
25413
|
-
},
|
|
25414
|
-
async pullSnapshot(options2, context) {
|
|
25415
|
-
const snapshot = await syncStore.pullSnapshot(options2);
|
|
25416
|
-
return importSnapshot(snapshot, context);
|
|
25417
|
-
},
|
|
25418
|
-
async syncOnce(options2, context) {
|
|
25419
|
-
const pulled = await remote.pullSnapshot(options2, context);
|
|
25420
|
-
const pushed = await remote.pushSnapshot(context);
|
|
25421
|
-
return { pulled, pushed };
|
|
25422
|
-
}
|
|
25423
|
-
};
|
|
25424
|
-
return {
|
|
25425
|
-
...local,
|
|
25426
|
-
kind: "hybrid",
|
|
25427
|
-
capabilities: {
|
|
25428
|
-
...local.capabilities,
|
|
25429
|
-
localPersistence: true,
|
|
25430
|
-
remotePersistence: true,
|
|
25431
|
-
sync: true
|
|
25432
|
-
},
|
|
25433
|
-
sync: {
|
|
25434
|
-
...local.sync,
|
|
25435
|
-
exportSnapshot,
|
|
25436
|
-
importSnapshot
|
|
25437
|
-
},
|
|
25438
|
-
remote
|
|
25439
|
-
};
|
|
25440
|
-
}
|
|
25441
|
-
|
|
25442
25763
|
// src/storage/postgres-adapter.ts
|
|
25443
|
-
init_types();
|
|
25444
|
-
import { randomUUID as randomUUID3 } from "crypto";
|
|
25445
25764
|
init_redaction();
|
|
25446
25765
|
function createPostgresTodosStorageAdapter(options) {
|
|
25447
25766
|
const store = new PostgresJsonRecordStore(options);
|
|
@@ -25569,6 +25888,9 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
25569
25888
|
exportSnapshot: () => exportSnapshot(store),
|
|
25570
25889
|
importSnapshot: (snapshot, context) => importSnapshot(snapshot, store, context)
|
|
25571
25890
|
},
|
|
25891
|
+
integrity: {
|
|
25892
|
+
report: () => store.integrityReport()
|
|
25893
|
+
},
|
|
25572
25894
|
transaction: (fn) => fn(adapter)
|
|
25573
25895
|
};
|
|
25574
25896
|
return adapter;
|
|
@@ -25675,8 +25997,9 @@ class PostgresJsonRecordStore {
|
|
|
25675
25997
|
conds.push(`payload->>'agent_id' = ${p(filter.agent_id)}`);
|
|
25676
25998
|
if (filter.session_id !== undefined)
|
|
25677
25999
|
conds.push(`payload->>'session_id' = ${p(filter.session_id)}`);
|
|
25678
|
-
if (filter.tags?.length)
|
|
25679
|
-
conds.push(`payload->'tags'
|
|
26000
|
+
if (filter.tags?.length) {
|
|
26001
|
+
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)})`);
|
|
26002
|
+
}
|
|
25680
26003
|
if (filter.has_recurrence !== undefined) {
|
|
25681
26004
|
conds.push(`(COALESCE(payload->>'recurrence_rule', '') <> '') = ${p(filter.has_recurrence)}`);
|
|
25682
26005
|
}
|
|
@@ -25737,7 +26060,8 @@ class PostgresJsonRecordStore {
|
|
|
25737
26060
|
AND object_id COLLATE "C" >= $2 AND object_id COLLATE "C" < $3
|
|
25738
26061
|
LIMIT 2`, [this.service, raw, upper]);
|
|
25739
26062
|
if (prefixResult.rows.length > 1) {
|
|
25740
|
-
|
|
26063
|
+
const candidates = prefixResult.rows.map((row) => payloadRecord2(row.payload));
|
|
26064
|
+
throw new TaskReferenceAmbiguousError(ref, candidates.map((task2) => ({ task_id: task2.id, project_id: task2.project_id })));
|
|
25741
26065
|
}
|
|
25742
26066
|
if (prefixResult.rows.length === 1) {
|
|
25743
26067
|
return payloadRecord2(prefixResult.rows[0].payload);
|
|
@@ -25748,7 +26072,8 @@ class PostgresJsonRecordStore {
|
|
|
25748
26072
|
AND LOWER(payload->>'short_id') = $2
|
|
25749
26073
|
LIMIT 2`, [this.service, raw]);
|
|
25750
26074
|
if (shortIdResult.rows.length > 1) {
|
|
25751
|
-
|
|
26075
|
+
const candidates = shortIdResult.rows.map((row) => payloadRecord2(row.payload));
|
|
26076
|
+
throw new TaskReferenceAmbiguousError(ref, candidates.map((task2) => ({ task_id: task2.id, project_id: task2.project_id })));
|
|
25752
26077
|
}
|
|
25753
26078
|
if (shortIdResult.rows.length === 1) {
|
|
25754
26079
|
return payloadRecord2(shortIdResult.rows[0].payload);
|
|
@@ -25762,6 +26087,24 @@ class PostgresJsonRecordStore {
|
|
|
25762
26087
|
const result = await this.options.client.query(sql, params);
|
|
25763
26088
|
return Number(result.rows[0]?.count ?? 0);
|
|
25764
26089
|
}
|
|
26090
|
+
async integrityReport() {
|
|
26091
|
+
await this.ensureSchema();
|
|
26092
|
+
const conditions = [];
|
|
26093
|
+
for (const spec of INTEGRITY_CONDITIONS) {
|
|
26094
|
+
const { sql, params } = buildPostgresIntegritySql(spec, { table: this.tableName, service: this.service });
|
|
26095
|
+
try {
|
|
26096
|
+
const result = await this.options.client.query(sql, params);
|
|
26097
|
+
const row = result.rows[0];
|
|
26098
|
+
conditions.push(measuredCondition(spec, {
|
|
26099
|
+
count: Number(row?.count ?? 0),
|
|
26100
|
+
open_count: spec.entity === "task" ? Number(row?.open_count ?? 0) : null
|
|
26101
|
+
}, "postgres"));
|
|
26102
|
+
} catch (error) {
|
|
26103
|
+
conditions.push(unverifiedCondition(spec, error instanceof Error ? error.message : String(error)));
|
|
26104
|
+
}
|
|
26105
|
+
}
|
|
26106
|
+
return buildIntegrityReport(conditions, new Date().toISOString());
|
|
26107
|
+
}
|
|
25765
26108
|
async listTombstones() {
|
|
25766
26109
|
await this.ensureSchema();
|
|
25767
26110
|
const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
|
|
@@ -26237,7 +26580,7 @@ async function startTask2(id, agentId, store) {
|
|
|
26237
26580
|
if (task2.status !== "pending" && task2.status !== "in_progress") {
|
|
26238
26581
|
throw new Error(`Task is ${task2.status} and cannot be started by ${agentId}`);
|
|
26239
26582
|
}
|
|
26240
|
-
|
|
26583
|
+
const started = await patchTask(task2, {
|
|
26241
26584
|
status: "in_progress",
|
|
26242
26585
|
assigned_to: task2.assigned_to ?? agentId,
|
|
26243
26586
|
agent_id: task2.agent_id ?? agentId,
|
|
@@ -26245,6 +26588,8 @@ async function startTask2(id, agentId, store) {
|
|
|
26245
26588
|
locked_at: new Date().toISOString(),
|
|
26246
26589
|
started_at: task2.started_at ?? new Date().toISOString()
|
|
26247
26590
|
}, store);
|
|
26591
|
+
await logTaskChange2(task2.id, "start", "status", task2.status, "in_progress", agentId, store);
|
|
26592
|
+
return started;
|
|
26248
26593
|
}
|
|
26249
26594
|
async function completeTask2(id, agentId, options, store) {
|
|
26250
26595
|
const task2 = await store.completeTask(id, agentId, options);
|
|
@@ -27353,17 +27698,17 @@ function createTodosCloudQueryClientFromEnv(env = process.env, options = {}) {
|
|
|
27353
27698
|
init_config();
|
|
27354
27699
|
function createTodosStorageAdapter(options = {}) {
|
|
27355
27700
|
const config = options.config ?? loadTodosStorageConfig(options.env);
|
|
27356
|
-
if (config.mode === "
|
|
27701
|
+
if (config.mode === "sqlite") {
|
|
27357
27702
|
if (isTodosShadowEnabled(options.env))
|
|
27358
27703
|
return createShadowAdapter(options, config);
|
|
27359
27704
|
return createLocalSqliteTodosStorageAdapter(options.local);
|
|
27360
27705
|
}
|
|
27361
27706
|
assertTodosRemoteStorageConfig(config);
|
|
27362
|
-
const adapter =
|
|
27707
|
+
const adapter = options.remoteAdapter ?? createImplicitPostgresAdapter(options);
|
|
27363
27708
|
if (!adapter) {
|
|
27364
|
-
throw new Error(
|
|
27709
|
+
throw new Error("postgres storage requires a repo-native Postgres adapter. " + "Pass remoteAdapter or postgresClient after wiring Postgres RDS support.");
|
|
27365
27710
|
}
|
|
27366
|
-
|
|
27711
|
+
assertPostgresAdapterCapabilities(adapter);
|
|
27367
27712
|
return adapter;
|
|
27368
27713
|
}
|
|
27369
27714
|
function createShadowAdapter(options, config) {
|
|
@@ -27377,7 +27722,7 @@ function createShadowAdapter(options, config) {
|
|
|
27377
27722
|
...options.local ? { local: options.local } : {},
|
|
27378
27723
|
...options.postgresSyncStore ? { syncStore: options.postgresSyncStore } : {},
|
|
27379
27724
|
...postgresClient ? { postgresClient } : {},
|
|
27380
|
-
...options.
|
|
27725
|
+
...options.sourceMachineId ? { sourceMachineId: options.sourceMachineId } : {}
|
|
27381
27726
|
});
|
|
27382
27727
|
}
|
|
27383
27728
|
function createImplicitPostgresAdapter(options) {
|
|
@@ -27385,25 +27730,12 @@ function createImplicitPostgresAdapter(options) {
|
|
|
27385
27730
|
return null;
|
|
27386
27731
|
return createPostgresTodosStorageAdapter({
|
|
27387
27732
|
client: options.postgresClient,
|
|
27388
|
-
...options.
|
|
27389
|
-
});
|
|
27390
|
-
}
|
|
27391
|
-
function createImplicitHybridAdapter(options) {
|
|
27392
|
-
if (!options.postgresClient && !options.postgresSyncStore)
|
|
27393
|
-
return null;
|
|
27394
|
-
return createHybridTodosStorageAdapter({
|
|
27395
|
-
...options.hybrid ?? {},
|
|
27396
|
-
local: options.local,
|
|
27397
|
-
postgresClient: options.postgresClient,
|
|
27398
|
-
syncStore: options.postgresSyncStore
|
|
27733
|
+
...options.sourceMachineId ? { sourceMachineId: options.sourceMachineId } : {}
|
|
27399
27734
|
});
|
|
27400
27735
|
}
|
|
27401
|
-
function
|
|
27736
|
+
function assertPostgresAdapterCapabilities(adapter) {
|
|
27402
27737
|
if (!adapter.capabilities.remotePersistence) {
|
|
27403
|
-
throw new Error(
|
|
27404
|
-
}
|
|
27405
|
-
if (mode === "hybrid" && !adapter.capabilities.localPersistence) {
|
|
27406
|
-
throw new Error("hybrid storage adapter must also set capabilities.localPersistence=true");
|
|
27738
|
+
throw new Error("postgres storage adapter must set capabilities.remotePersistence=true");
|
|
27407
27739
|
}
|
|
27408
27740
|
}
|
|
27409
27741
|
// src/storage/shadow-outbox.ts
|
|
@@ -27730,6 +28062,50 @@ async function closeRuntimeShadowCloud() {
|
|
|
27730
28062
|
await cloud.close().catch(() => {});
|
|
27731
28063
|
}
|
|
27732
28064
|
}
|
|
28065
|
+
// src/storage/hybrid.ts
|
|
28066
|
+
function createHybridTodosStorageAdapter(options) {
|
|
28067
|
+
const local = options.localAdapter ?? createLocalSqliteTodosStorageAdapter(options.local);
|
|
28068
|
+
const syncStore = options.syncStore ?? (options.postgresClient ? createPostgresTodosSyncStore(options.postgresClient, { sourceMachineId: options.sourceMachineId }) : null);
|
|
28069
|
+
if (!syncStore)
|
|
28070
|
+
throw new Error("hybrid storage requires a Postgres sync store or query client");
|
|
28071
|
+
if (!local.sync.exportSnapshot || !local.sync.importSnapshot) {
|
|
28072
|
+
throw new Error("hybrid storage requires local snapshot export/import support");
|
|
28073
|
+
}
|
|
28074
|
+
const exportSnapshot2 = async (context) => await local.sync.exportSnapshot(context);
|
|
28075
|
+
const importSnapshot2 = async (snapshot, context) => await local.sync.importSnapshot(snapshot, context);
|
|
28076
|
+
const remote = {
|
|
28077
|
+
ensureSchema: () => syncStore.ensureSchema(),
|
|
28078
|
+
async pushSnapshot(context) {
|
|
28079
|
+
const snapshot = await exportSnapshot2(context);
|
|
28080
|
+
return syncStore.pushSnapshot(snapshot, context);
|
|
28081
|
+
},
|
|
28082
|
+
async pullSnapshot(options2, context) {
|
|
28083
|
+
const snapshot = await syncStore.pullSnapshot(options2);
|
|
28084
|
+
return importSnapshot2(snapshot, context);
|
|
28085
|
+
},
|
|
28086
|
+
async syncOnce(options2, context) {
|
|
28087
|
+
const pulled = await remote.pullSnapshot(options2, context);
|
|
28088
|
+
const pushed = await remote.pushSnapshot(context);
|
|
28089
|
+
return { pulled, pushed };
|
|
28090
|
+
}
|
|
28091
|
+
};
|
|
28092
|
+
return {
|
|
28093
|
+
...local,
|
|
28094
|
+
kind: "hybrid",
|
|
28095
|
+
capabilities: {
|
|
28096
|
+
...local.capabilities,
|
|
28097
|
+
localPersistence: true,
|
|
28098
|
+
remotePersistence: true,
|
|
28099
|
+
sync: true
|
|
28100
|
+
},
|
|
28101
|
+
sync: {
|
|
28102
|
+
...local.sync,
|
|
28103
|
+
exportSnapshot: exportSnapshot2,
|
|
28104
|
+
importSnapshot: importSnapshot2
|
|
28105
|
+
},
|
|
28106
|
+
remote
|
|
28107
|
+
};
|
|
28108
|
+
}
|
|
27733
28109
|
// src/storage/comment-redaction-backfill.ts
|
|
27734
28110
|
init_redaction();
|
|
27735
28111
|
var COMMENT_REDACTION_BACKFILL_CONFIRMATION = "REDACT_STORED_TODOS_COMMENTS";
|
|
@@ -31308,10 +31684,10 @@ function getNativeStorageStatus(env = process.env) {
|
|
|
31308
31684
|
} catch (error) {
|
|
31309
31685
|
issues.push(error instanceof Error ? error.message : String(error));
|
|
31310
31686
|
}
|
|
31311
|
-
const remoteEnabled =
|
|
31687
|
+
const remoteEnabled = isTodosPostgresBackend(config);
|
|
31312
31688
|
const remoteFieldsConfigured = Boolean(config.database || config.objectStorage);
|
|
31313
31689
|
if (!remoteEnabled && remoteFieldsConfigured) {
|
|
31314
|
-
warnings.push(
|
|
31690
|
+
warnings.push(`the sqlite backend (${TODOS_STORAGE_ENV.mode}) ignores configured remote storage fields`);
|
|
31315
31691
|
}
|
|
31316
31692
|
if (remoteEnabled && !config.objectStorage) {
|
|
31317
31693
|
warnings.push(`${TODOS_STORAGE_ENV.s3Bucket} is not configured, so artifact sync will stay local`);
|
|
@@ -31320,7 +31696,7 @@ function getNativeStorageStatus(env = process.env) {
|
|
|
31320
31696
|
ok: issues.length === 0,
|
|
31321
31697
|
service: "todos",
|
|
31322
31698
|
mode: config.mode,
|
|
31323
|
-
local_default: config.mode === "
|
|
31699
|
+
local_default: config.mode === "sqlite",
|
|
31324
31700
|
remote_enabled: remoteEnabled,
|
|
31325
31701
|
database: {
|
|
31326
31702
|
configured: Boolean(config.database),
|
|
@@ -31402,8 +31778,12 @@ function storageEnvStatus(env) {
|
|
|
31402
31778
|
]));
|
|
31403
31779
|
}
|
|
31404
31780
|
function fallbackConfig(env) {
|
|
31405
|
-
|
|
31406
|
-
|
|
31781
|
+
let mode;
|
|
31782
|
+
try {
|
|
31783
|
+
mode = parseStorageBackend(clean2(env[TODOS_STORAGE_ENV.mode]));
|
|
31784
|
+
} catch {
|
|
31785
|
+
mode = "sqlite";
|
|
31786
|
+
}
|
|
31407
31787
|
return {
|
|
31408
31788
|
service: "todos",
|
|
31409
31789
|
mode,
|
|
@@ -50582,7 +50962,7 @@ function parseObject4(value) {
|
|
|
50582
50962
|
function normalizeKey(value) {
|
|
50583
50963
|
return value.trim().toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
50584
50964
|
}
|
|
50585
|
-
function
|
|
50965
|
+
function normalizeTaskFindingFingerprint(value) {
|
|
50586
50966
|
const normalized = normalizeKey(value);
|
|
50587
50967
|
if (!normalized)
|
|
50588
50968
|
throw new Error("finding fingerprint is required");
|
|
@@ -50691,7 +51071,7 @@ function assertTask(taskId, db) {
|
|
|
50691
51071
|
throw new TaskNotFoundError(taskId);
|
|
50692
51072
|
}
|
|
50693
51073
|
function nextFinding(input, db) {
|
|
50694
|
-
const fingerprint4 =
|
|
51074
|
+
const fingerprint4 = normalizeTaskFindingFingerprint(input.fingerprint);
|
|
50695
51075
|
const title = redactOptional(input.title, 300);
|
|
50696
51076
|
if (!title)
|
|
50697
51077
|
throw new Error("finding title is required");
|
|
@@ -50839,7 +51219,7 @@ function resolveMissingTaskFindings(input, db) {
|
|
|
50839
51219
|
const timestamp3 = input.resolved_at || now();
|
|
50840
51220
|
const status = normalizeResolutionStatus(input.status);
|
|
50841
51221
|
const runId = resolveRunForTask(input.run_id, input.task_id, d);
|
|
50842
|
-
const present = new Set(input.fingerprints.map(
|
|
51222
|
+
const present = new Set(input.fingerprints.map(normalizeTaskFindingFingerprint));
|
|
50843
51223
|
const warnings = [];
|
|
50844
51224
|
const conditions = ["task_id = ?", "status = 'open'"];
|
|
50845
51225
|
const params = [input.task_id];
|
|
@@ -51366,12 +51746,12 @@ init_artifact_store();
|
|
|
51366
51746
|
|
|
51367
51747
|
// src/lib/doctor.ts
|
|
51368
51748
|
init_database();
|
|
51749
|
+
import { chmodSync, copyFileSync as copyFileSync2, existsSync as existsSync30, mkdirSync as mkdirSync22, statSync as statSync12 } from "fs";
|
|
51750
|
+
import { basename as basename7, dirname as dirname18, join as join27 } from "path";
|
|
51369
51751
|
init_migrations();
|
|
51370
51752
|
init_schema();
|
|
51371
51753
|
init_recurrence();
|
|
51372
|
-
|
|
51373
|
-
import { basename as basename7, dirname as dirname18, join as join27 } from "path";
|
|
51374
|
-
var REQUIRED_TABLES2 = [
|
|
51754
|
+
var REQUIRED_TABLES3 = [
|
|
51375
51755
|
"_migrations",
|
|
51376
51756
|
"projects",
|
|
51377
51757
|
"tasks",
|
|
@@ -51384,7 +51764,7 @@ var REQUIRED_TABLES2 = [
|
|
|
51384
51764
|
"task_run_commands",
|
|
51385
51765
|
"task_run_artifacts"
|
|
51386
51766
|
];
|
|
51387
|
-
function
|
|
51767
|
+
function tableExists2(db, table) {
|
|
51388
51768
|
return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
|
|
51389
51769
|
}
|
|
51390
51770
|
function quoteIdent(identifier) {
|
|
@@ -51468,7 +51848,7 @@ function findDuplicateIndexes(db) {
|
|
|
51468
51848
|
return duplicates;
|
|
51469
51849
|
}
|
|
51470
51850
|
function findMissingProjectRoots(db) {
|
|
51471
|
-
if (!
|
|
51851
|
+
if (!tableExists2(db, "projects"))
|
|
51472
51852
|
return 0;
|
|
51473
51853
|
let missing = 0;
|
|
51474
51854
|
const rows = db.query("SELECT path FROM projects WHERE path IS NOT NULL AND path != ''").all();
|
|
@@ -51483,7 +51863,7 @@ function findMissingProjectRoots(db) {
|
|
|
51483
51863
|
return missing;
|
|
51484
51864
|
}
|
|
51485
51865
|
function addTaskStateChecks(db, checks) {
|
|
51486
|
-
if (!
|
|
51866
|
+
if (!tableExists2(db, "tasks"))
|
|
51487
51867
|
return;
|
|
51488
51868
|
const columns = getTableColumns(db, "tasks");
|
|
51489
51869
|
const staleCutoff = new Date(Date.now() - 30 * 60 * 1000).toISOString();
|
|
@@ -51555,15 +51935,42 @@ function createBackup(dbPath) {
|
|
|
51555
51935
|
function pushRepair(repairs, type, message, applied, count3) {
|
|
51556
51936
|
repairs.push({ type, message, applied, count: count3 });
|
|
51557
51937
|
}
|
|
51558
|
-
function summarize3(checks, repairs) {
|
|
51938
|
+
function summarize3(checks, repairs, integrity) {
|
|
51559
51939
|
return {
|
|
51560
51940
|
errors: checks.filter((check) => check.severity === "error").length,
|
|
51561
51941
|
warnings: checks.filter((check) => check.severity === "warn").length,
|
|
51562
51942
|
infos: checks.filter((check) => check.severity === "info").length,
|
|
51563
51943
|
repairable: checks.filter((check) => check.repairable).length,
|
|
51564
|
-
applied: repairs.filter((repair) => repair.applied).length
|
|
51944
|
+
applied: repairs.filter((repair) => repair.applied).length,
|
|
51945
|
+
integrity_findings: integrity.summary.findings,
|
|
51946
|
+
integrity_rows: integrity.summary.rows,
|
|
51947
|
+
integrity_unverified: integrity.summary.unverified
|
|
51565
51948
|
};
|
|
51566
51949
|
}
|
|
51950
|
+
function addIntegrityChecks(integrity, checks) {
|
|
51951
|
+
for (const condition of integrity.conditions) {
|
|
51952
|
+
if (!condition.verified) {
|
|
51953
|
+
addCheck(checks, {
|
|
51954
|
+
severity: "warn",
|
|
51955
|
+
type: `${condition.id}_unverified`,
|
|
51956
|
+
message: condition.message,
|
|
51957
|
+
repairable: false,
|
|
51958
|
+
integrity: true
|
|
51959
|
+
});
|
|
51960
|
+
continue;
|
|
51961
|
+
}
|
|
51962
|
+
if ((condition.count ?? 0) === 0)
|
|
51963
|
+
continue;
|
|
51964
|
+
addCheck(checks, {
|
|
51965
|
+
severity: condition.severity ?? "warn",
|
|
51966
|
+
type: condition.id,
|
|
51967
|
+
message: `${condition.message} \u2014 ${condition.impact}`,
|
|
51968
|
+
count: condition.count ?? undefined,
|
|
51969
|
+
repairable: false,
|
|
51970
|
+
integrity: true
|
|
51971
|
+
});
|
|
51972
|
+
}
|
|
51973
|
+
}
|
|
51567
51974
|
function deleteOrphans(db, table, where) {
|
|
51568
51975
|
const before = countQuery2(db, `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`);
|
|
51569
51976
|
if (before > 0)
|
|
@@ -51592,7 +51999,7 @@ function runTodosDoctor(options = {}) {
|
|
|
51592
51999
|
message: `Schema at migration ${migrationCurrent}`
|
|
51593
52000
|
});
|
|
51594
52001
|
}
|
|
51595
|
-
const missingTables =
|
|
52002
|
+
const missingTables = REQUIRED_TABLES3.filter((table) => !tableExists2(db, table));
|
|
51596
52003
|
if (missingTables.length > 0) {
|
|
51597
52004
|
addCheck(checks, {
|
|
51598
52005
|
severity: "error",
|
|
@@ -51602,7 +52009,7 @@ function runTodosDoctor(options = {}) {
|
|
|
51602
52009
|
repairable: true
|
|
51603
52010
|
});
|
|
51604
52011
|
}
|
|
51605
|
-
const orphanedParents =
|
|
52012
|
+
const orphanedParents = tableExists2(db, "tasks") ? countQuery2(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;
|
|
51606
52013
|
if (orphanedParents > 0) {
|
|
51607
52014
|
addCheck(checks, {
|
|
51608
52015
|
severity: "error",
|
|
@@ -51612,7 +52019,7 @@ function runTodosDoctor(options = {}) {
|
|
|
51612
52019
|
repairable: true
|
|
51613
52020
|
});
|
|
51614
52021
|
}
|
|
51615
|
-
const orphanedDependencies =
|
|
52022
|
+
const orphanedDependencies = tableExists2(db, "task_dependencies") && tableExists2(db, "tasks") ? countQuery2(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;
|
|
51616
52023
|
if (orphanedDependencies > 0) {
|
|
51617
52024
|
addCheck(checks, {
|
|
51618
52025
|
severity: "error",
|
|
@@ -51631,7 +52038,7 @@ function runTodosDoctor(options = {}) {
|
|
|
51631
52038
|
];
|
|
51632
52039
|
let orphanedRows2 = 0;
|
|
51633
52040
|
for (const [table, where] of orphanTables) {
|
|
51634
|
-
if (!
|
|
52041
|
+
if (!tableExists2(db, table))
|
|
51635
52042
|
continue;
|
|
51636
52043
|
orphanedRows2 += countQuery2(db, `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`);
|
|
51637
52044
|
}
|
|
@@ -51645,6 +52052,8 @@ function runTodosDoctor(options = {}) {
|
|
|
51645
52052
|
});
|
|
51646
52053
|
}
|
|
51647
52054
|
addTaskStateChecks(db, checks);
|
|
52055
|
+
const integrity = scanSqliteIntegrity(db);
|
|
52056
|
+
addIntegrityChecks(integrity, checks);
|
|
51648
52057
|
const corruptJson = findCorruptJsonMetadata(db);
|
|
51649
52058
|
if (corruptJson.length > 0) {
|
|
51650
52059
|
addCheck(checks, {
|
|
@@ -51703,12 +52112,12 @@ function runTodosDoctor(options = {}) {
|
|
|
51703
52112
|
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)");
|
|
51704
52113
|
pushRepair(repairs, "orphaned_task_parents", "Cleared missing parent references", true, orphanedParents);
|
|
51705
52114
|
}
|
|
51706
|
-
if (orphanedDependencies > 0 &&
|
|
52115
|
+
if (orphanedDependencies > 0 && tableExists2(db, "task_dependencies")) {
|
|
51707
52116
|
const count3 = 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)");
|
|
51708
52117
|
pushRepair(repairs, "orphaned_task_dependencies", "Deleted dependency rows referencing missing tasks", true, count3);
|
|
51709
52118
|
}
|
|
51710
52119
|
for (const [table, where] of orphanTables) {
|
|
51711
|
-
if (!
|
|
52120
|
+
if (!tableExists2(db, table))
|
|
51712
52121
|
continue;
|
|
51713
52122
|
const count3 = deleteOrphans(db, table, where);
|
|
51714
52123
|
if (count3 > 0)
|
|
@@ -51737,16 +52146,19 @@ function runTodosDoctor(options = {}) {
|
|
|
51737
52146
|
}
|
|
51738
52147
|
}
|
|
51739
52148
|
}
|
|
51740
|
-
const
|
|
51741
|
-
const
|
|
52149
|
+
const reread = apply && hasRepairableIssue ? runTodosDoctor({ db, dbPath, apply: false }) : undefined;
|
|
52150
|
+
const finalChecks = reread?.checks ?? checks;
|
|
52151
|
+
const finalIntegrity = reread?.integrity ?? integrity;
|
|
52152
|
+
const summary = summarize3(finalChecks, repairs, finalIntegrity);
|
|
51742
52153
|
return {
|
|
51743
|
-
ok: !finalChecks.some((check) => check.severity === "error"),
|
|
52154
|
+
ok: !finalChecks.some((check) => check.severity === "error") && finalIntegrity.summary.ok,
|
|
51744
52155
|
dry_run: !apply,
|
|
51745
52156
|
database_path: dbPath,
|
|
51746
52157
|
migration: { current: getMigrationLevel(db), expected: migrationExpected },
|
|
51747
52158
|
backup,
|
|
51748
52159
|
checks: finalChecks,
|
|
51749
52160
|
repairs,
|
|
52161
|
+
integrity: finalIntegrity,
|
|
51750
52162
|
summary
|
|
51751
52163
|
};
|
|
51752
52164
|
}
|
|
@@ -52292,6 +52704,7 @@ export {
|
|
|
52292
52704
|
updateArtifactRedaction,
|
|
52293
52705
|
updateAgentActivity,
|
|
52294
52706
|
updateAgent,
|
|
52707
|
+
unverifiedCondition,
|
|
52295
52708
|
unsubscribeResource,
|
|
52296
52709
|
unlockTask,
|
|
52297
52710
|
unlinkTaskCommit,
|
|
@@ -52312,6 +52725,7 @@ export {
|
|
|
52312
52725
|
summarizeTaskRun,
|
|
52313
52726
|
summarizeRoadmap,
|
|
52314
52727
|
summarizeMilestone,
|
|
52728
|
+
summarizeIntegrity,
|
|
52315
52729
|
suggestAgentNames,
|
|
52316
52730
|
subscribeResource,
|
|
52317
52731
|
storeArtifactContent,
|
|
@@ -52352,6 +52766,7 @@ export {
|
|
|
52352
52766
|
scorePlanHealth,
|
|
52353
52767
|
scheduleTask,
|
|
52354
52768
|
scanTextForSecrets,
|
|
52769
|
+
scanSqliteIntegrity,
|
|
52355
52770
|
scanSourceForForbiddenWebPatterns,
|
|
52356
52771
|
scanReminders,
|
|
52357
52772
|
scanPreWriteText,
|
|
@@ -52399,6 +52814,7 @@ export {
|
|
|
52399
52814
|
resolvePartialId,
|
|
52400
52815
|
resolveMissingTaskFindings,
|
|
52401
52816
|
resolveMentions,
|
|
52817
|
+
resolveIntegritySeverity,
|
|
52402
52818
|
resolveGitRoot,
|
|
52403
52819
|
resolveCommandQuery,
|
|
52404
52820
|
resolveAccessProfile,
|
|
@@ -52513,6 +52929,7 @@ export {
|
|
|
52513
52929
|
patrolTasks,
|
|
52514
52930
|
parseTmuxTarget,
|
|
52515
52931
|
parseStorageMode,
|
|
52932
|
+
parseStorageBackend,
|
|
52516
52933
|
parseRecurrenceRule,
|
|
52517
52934
|
parseQuietHours,
|
|
52518
52935
|
parsePlanArtifactMarkdown,
|
|
@@ -52541,6 +52958,8 @@ export {
|
|
|
52541
52958
|
migrationDryRun,
|
|
52542
52959
|
migrateWorkflowStates,
|
|
52543
52960
|
mergeDuplicateTask,
|
|
52961
|
+
measuredCondition,
|
|
52962
|
+
measureIntegrityRows,
|
|
52544
52963
|
materializePlanSteps,
|
|
52545
52964
|
matchCapabilities,
|
|
52546
52965
|
looksSensitiveKey,
|
|
@@ -52659,12 +53078,14 @@ export {
|
|
|
52659
53078
|
isWorktreePath,
|
|
52660
53079
|
isValidRecurrenceRule,
|
|
52661
53080
|
isTodosRemoteStorageEnabled,
|
|
53081
|
+
isTodosPostgresBackend,
|
|
52662
53082
|
isSnapshotStale,
|
|
52663
53083
|
isEncryptedValue,
|
|
52664
53084
|
isEncryptedBridgeBundle,
|
|
52665
53085
|
isArtifactExpired,
|
|
52666
53086
|
isAllowedLocalApiUrl,
|
|
52667
53087
|
isAgentConflict,
|
|
53088
|
+
integrityCondition,
|
|
52668
53089
|
installTemplateLibrary,
|
|
52669
53090
|
installLocalExtension,
|
|
52670
53091
|
inspectPlanArtifact,
|
|
@@ -52707,6 +53128,7 @@ export {
|
|
|
52707
53128
|
getTodosStorageEnvName,
|
|
52708
53129
|
getTodosStorageDatabaseUrl,
|
|
52709
53130
|
getTodosStorageDatabaseEnv,
|
|
53131
|
+
getTodosStorageBackend,
|
|
52710
53132
|
getTimeReport,
|
|
52711
53133
|
getTimeLogs,
|
|
52712
53134
|
getTerminalNotificationRule,
|
|
@@ -52914,6 +53336,7 @@ export {
|
|
|
52914
53336
|
formatLockConflict,
|
|
52915
53337
|
formatKnowledgeSnapshotMarkdown,
|
|
52916
53338
|
formatIssueImportPreviewText,
|
|
53339
|
+
formatIntegrityMessage,
|
|
52917
53340
|
formatIntakePreviewText,
|
|
52918
53341
|
formatHandoffPacket,
|
|
52919
53342
|
formatGoalHandoff,
|
|
@@ -53144,16 +53567,19 @@ export {
|
|
|
53144
53567
|
bulkAddTaskFiles,
|
|
53145
53568
|
buildTaskDependencyEdges,
|
|
53146
53569
|
buildTaskBoardSnapshot,
|
|
53570
|
+
buildSqliteIntegritySql,
|
|
53147
53571
|
buildS3ObjectUrl,
|
|
53148
53572
|
buildS3ObjectKey,
|
|
53149
53573
|
buildRunReplayBundle,
|
|
53150
53574
|
buildResourceSnapshot,
|
|
53151
53575
|
buildReportExportData,
|
|
53152
53576
|
buildProjectDependencyGraph,
|
|
53577
|
+
buildPostgresIntegritySql,
|
|
53153
53578
|
buildPlanArtifactSnapshot,
|
|
53154
53579
|
buildMcpToolGroups,
|
|
53155
53580
|
buildMachineTopologyReport,
|
|
53156
53581
|
buildKnowledgeSnapshotPayload,
|
|
53582
|
+
buildIntegrityReport,
|
|
53157
53583
|
buildHandoffPacket,
|
|
53158
53584
|
buildFeatureManifest,
|
|
53159
53585
|
buildFailureTriageReport,
|
|
@@ -53188,6 +53614,7 @@ export {
|
|
|
53188
53614
|
appendRunCommand,
|
|
53189
53615
|
analyzeDependencyGraph,
|
|
53190
53616
|
agentClaimNextSafe,
|
|
53617
|
+
adoptRemoteIntegrityReport,
|
|
53191
53618
|
addTemplateTasks,
|
|
53192
53619
|
addTaskRunFile,
|
|
53193
53620
|
addTaskRunEvent,
|
|
@@ -53234,6 +53661,7 @@ export {
|
|
|
53234
53661
|
TODOS_LOCAL_BACKUP_KIND,
|
|
53235
53662
|
TODOS_JSON_CONTRACTS_MANIFEST,
|
|
53236
53663
|
TODOS_JSON_CONTRACTS,
|
|
53664
|
+
TODOS_INTEGRITY_SCHEMA_VERSION,
|
|
53237
53665
|
TODOS_ERROR_CODES,
|
|
53238
53666
|
TODOS_ENCRYPTION_SCHEMA_VERSION,
|
|
53239
53667
|
TODOS_ENCRYPTED_VALUE_KIND,
|
|
@@ -53290,6 +53718,7 @@ export {
|
|
|
53290
53718
|
PLAN_EXECUTION_SCHEMA,
|
|
53291
53719
|
PLAN_EXECUTION_MODES,
|
|
53292
53720
|
PARITY_SCHEMA_VERSION,
|
|
53721
|
+
OPEN_TASK_STATUSES,
|
|
53293
53722
|
NOTIFICATION_REMINDERS_SCHEMA,
|
|
53294
53723
|
NL_INTAKE_SCHEMA,
|
|
53295
53724
|
NESTED_SUBCOMMANDS,
|
|
@@ -53318,6 +53747,7 @@ export {
|
|
|
53318
53747
|
JSON_OUTPUT_CONTRACT,
|
|
53319
53748
|
ISSUE_SOURCES,
|
|
53320
53749
|
ISSUE_IMPORT_SCHEMA,
|
|
53750
|
+
INTEGRITY_CONDITIONS,
|
|
53321
53751
|
INTAKE_TRIAGE_STATUSES,
|
|
53322
53752
|
INTAKE_SOURCE_TYPES,
|
|
53323
53753
|
INBOX_INTAKE_SCHEMA,
|