@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/index.js
CHANGED
|
@@ -3424,7 +3424,10 @@ var init_machines = __esm(() => {
|
|
|
3424
3424
|
});
|
|
3425
3425
|
|
|
3426
3426
|
// src/types/index.ts
|
|
3427
|
-
|
|
3427
|
+
function isBlockingDependencyStatus(status) {
|
|
3428
|
+
return status !== "completed" && status !== "cancelled";
|
|
3429
|
+
}
|
|
3430
|
+
var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
|
|
3428
3431
|
var init_types = __esm(() => {
|
|
3429
3432
|
TASK_STATUSES = [
|
|
3430
3433
|
"pending",
|
|
@@ -3464,6 +3467,22 @@ var init_types = __esm(() => {
|
|
|
3464
3467
|
this.name = "TaskNotFoundError";
|
|
3465
3468
|
}
|
|
3466
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
|
+
};
|
|
3467
3486
|
ProjectNotFoundError = class ProjectNotFoundError extends Error {
|
|
3468
3487
|
projectId;
|
|
3469
3488
|
static code = "PROJECT_NOT_FOUND";
|
|
@@ -3755,15 +3774,18 @@ var init_identity_mapping = __esm(() => {
|
|
|
3755
3774
|
var exports_config = {};
|
|
3756
3775
|
__export(exports_config, {
|
|
3757
3776
|
parseStorageMode: () => parseStorageMode,
|
|
3777
|
+
parseStorageBackend: () => parseStorageBackend,
|
|
3758
3778
|
loadTodosStorageConfig: () => loadTodosStorageConfig,
|
|
3759
3779
|
loadStorageConfig: () => loadStorageConfig,
|
|
3760
3780
|
isTodosShadowEnabled: () => isTodosShadowEnabled,
|
|
3761
3781
|
isTodosRemoteStorageEnabled: () => isTodosRemoteStorageEnabled,
|
|
3782
|
+
isTodosPostgresBackend: () => isTodosPostgresBackend,
|
|
3762
3783
|
getTodosStorageShadowEnvName: () => getTodosStorageShadowEnvName,
|
|
3763
3784
|
getTodosStorageMode: () => getTodosStorageMode,
|
|
3764
3785
|
getTodosStorageEnvName: () => getTodosStorageEnvName,
|
|
3765
3786
|
getTodosStorageDatabaseUrl: () => getTodosStorageDatabaseUrl,
|
|
3766
3787
|
getTodosStorageDatabaseEnv: () => getTodosStorageDatabaseEnv,
|
|
3788
|
+
getTodosStorageBackend: () => getTodosStorageBackend,
|
|
3767
3789
|
getStorageMode: () => getStorageMode,
|
|
3768
3790
|
getStorageDatabaseUrl: () => getStorageDatabaseUrl,
|
|
3769
3791
|
getStorageDatabaseEnv: () => getStorageDatabaseEnv,
|
|
@@ -3788,7 +3810,7 @@ function getCanonicalTodosRdsConfig(env = process.env) {
|
|
|
3788
3810
|
};
|
|
3789
3811
|
}
|
|
3790
3812
|
function loadTodosStorageConfig(env = process.env) {
|
|
3791
|
-
const mode =
|
|
3813
|
+
const mode = getTodosStorageBackend(env);
|
|
3792
3814
|
const databaseUrl = getTodosStorageDatabaseUrl(env);
|
|
3793
3815
|
const bucket = readStorageEnv(env, "s3Bucket").value;
|
|
3794
3816
|
const prefix = readStorageEnv(env, "s3Prefix").value ?? "todos/";
|
|
@@ -3825,29 +3847,41 @@ function loadTodosStorageConfig(env = process.env) {
|
|
|
3825
3847
|
function loadStorageConfig(env = process.env) {
|
|
3826
3848
|
return loadTodosStorageConfig(env);
|
|
3827
3849
|
}
|
|
3850
|
+
function isTodosPostgresBackend(config) {
|
|
3851
|
+
return config.mode === "postgres";
|
|
3852
|
+
}
|
|
3828
3853
|
function isTodosRemoteStorageEnabled(config) {
|
|
3829
|
-
return config
|
|
3854
|
+
return isTodosPostgresBackend(config);
|
|
3830
3855
|
}
|
|
3831
3856
|
function assertTodosRemoteStorageConfig(config) {
|
|
3832
|
-
if (!
|
|
3857
|
+
if (!isTodosPostgresBackend(config))
|
|
3833
3858
|
return;
|
|
3834
3859
|
if (!config.database?.url) {
|
|
3835
3860
|
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${TODOS_STORAGE_ENV.mode}=${config.mode}`);
|
|
3836
3861
|
}
|
|
3837
3862
|
}
|
|
3838
|
-
function
|
|
3863
|
+
function parseStorageBackend(value) {
|
|
3839
3864
|
const normalized = clean(value)?.toLowerCase();
|
|
3840
3865
|
if (!normalized)
|
|
3841
|
-
return "
|
|
3842
|
-
if (normalized === "
|
|
3866
|
+
return "sqlite";
|
|
3867
|
+
if (normalized === "sqlite" || normalized === "postgres")
|
|
3843
3868
|
return normalized;
|
|
3844
|
-
|
|
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);
|
|
3845
3879
|
}
|
|
3846
3880
|
function getTodosStorageMode(env = process.env) {
|
|
3847
|
-
return
|
|
3881
|
+
return getTodosStorageBackend(env);
|
|
3848
3882
|
}
|
|
3849
3883
|
function getStorageMode(env = process.env) {
|
|
3850
|
-
return
|
|
3884
|
+
return getTodosStorageBackend(env);
|
|
3851
3885
|
}
|
|
3852
3886
|
function isTodosShadowEnabled(env = process.env) {
|
|
3853
3887
|
return parseBoolean(readStorageEnv(env, "shadow").value, false);
|
|
@@ -3858,8 +3892,8 @@ function getTodosStorageShadowEnvName(env = process.env) {
|
|
|
3858
3892
|
function assertTodosShadowConfig(config, env = process.env) {
|
|
3859
3893
|
if (!isTodosShadowEnabled(env))
|
|
3860
3894
|
return;
|
|
3861
|
-
if (config.mode !== "
|
|
3862
|
-
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})`);
|
|
3863
3897
|
}
|
|
3864
3898
|
if (!config.database?.url) {
|
|
3865
3899
|
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${readStorageEnv(env, "shadow").name} is enabled`);
|
|
@@ -3915,7 +3949,7 @@ function parsePositiveInteger(value, fallback) {
|
|
|
3915
3949
|
}
|
|
3916
3950
|
return parsed;
|
|
3917
3951
|
}
|
|
3918
|
-
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;
|
|
3919
3953
|
var init_config = __esm(() => {
|
|
3920
3954
|
TODOS_STORAGE_TABLES = [
|
|
3921
3955
|
"todos_sync_records",
|
|
@@ -3956,6 +3990,14 @@ var init_config = __esm(() => {
|
|
|
3956
3990
|
syncBatchSize: "TODOS_SYNC_BATCH_SIZE",
|
|
3957
3991
|
syncDryRun: "TODOS_SYNC_DRY_RUN"
|
|
3958
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
|
+
};
|
|
3959
4001
|
});
|
|
3960
4002
|
|
|
3961
4003
|
// src/storage/shadow-outbox-schema.ts
|
|
@@ -4089,6 +4131,30 @@ function findGitRoot(startDir) {
|
|
|
4089
4131
|
}
|
|
4090
4132
|
return null;
|
|
4091
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
|
+
}
|
|
4092
4158
|
function getDbPath() {
|
|
4093
4159
|
if (process.env["HASNA_TODOS_DB_PATH"]) {
|
|
4094
4160
|
return process.env["HASNA_TODOS_DB_PATH"];
|
|
@@ -4096,18 +4162,19 @@ function getDbPath() {
|
|
|
4096
4162
|
if (process.env["TODOS_DB_PATH"]) {
|
|
4097
4163
|
return process.env["TODOS_DB_PATH"];
|
|
4098
4164
|
}
|
|
4165
|
+
if (hasExplicitProjectArg())
|
|
4166
|
+
return getGlobalDbPath();
|
|
4099
4167
|
const cwd = process.cwd();
|
|
4100
4168
|
const nearest = findNearestProjectDb(cwd);
|
|
4101
4169
|
if (nearest)
|
|
4102
4170
|
return nearest;
|
|
4103
4171
|
if (process.env["TODOS_DB_SCOPE"] === "project") {
|
|
4104
4172
|
const gitRoot = findGitRoot(cwd);
|
|
4105
|
-
if (gitRoot) {
|
|
4173
|
+
if (gitRoot && canCreateScopedProjectDb()) {
|
|
4106
4174
|
return join(gitRoot, ".hasna", "todos", "todos.db");
|
|
4107
4175
|
}
|
|
4108
4176
|
}
|
|
4109
|
-
|
|
4110
|
-
return join(home, ".hasna", "todos", "todos.db");
|
|
4177
|
+
return getGlobalDbPath();
|
|
4111
4178
|
}
|
|
4112
4179
|
function getDatabasePath() {
|
|
4113
4180
|
return getDbPath();
|
|
@@ -4123,8 +4190,8 @@ function ensureDir(filePath) {
|
|
|
4123
4190
|
function openDatabase(path) {
|
|
4124
4191
|
ensureDir(path);
|
|
4125
4192
|
const db = new Database(path);
|
|
4126
|
-
db.run("PRAGMA journal_mode = WAL");
|
|
4127
4193
|
db.run("PRAGMA busy_timeout = 5000");
|
|
4194
|
+
db.run("PRAGMA journal_mode = WAL");
|
|
4128
4195
|
db.run("PRAGMA foreign_keys = ON");
|
|
4129
4196
|
runMigrations(db);
|
|
4130
4197
|
ensureAgentIdentitySchema(db);
|
|
@@ -4210,10 +4277,13 @@ function resolvePartialId(db, table, partialId) {
|
|
|
4210
4277
|
return null;
|
|
4211
4278
|
}
|
|
4212
4279
|
if (table === "tasks") {
|
|
4213
|
-
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);
|
|
4214
4281
|
if (shortIdRows.length === 1) {
|
|
4215
4282
|
return shortIdRows[0].id;
|
|
4216
4283
|
}
|
|
4284
|
+
if (shortIdRows.length > 1) {
|
|
4285
|
+
throw new TaskReferenceAmbiguousError(partialId, shortIdRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
|
|
4286
|
+
}
|
|
4217
4287
|
}
|
|
4218
4288
|
if (table === "task_lists") {
|
|
4219
4289
|
const slugRow = db.query("SELECT id FROM task_lists WHERE slug = ?").get(partialId);
|
|
@@ -4955,11 +5025,15 @@ function secretPatterns() {
|
|
|
4955
5025
|
return [...customPatterns(), ...DEFAULT_SECRET_PATTERNS];
|
|
4956
5026
|
}
|
|
4957
5027
|
function isRedactionPlaceholderMatch(match) {
|
|
4958
|
-
const placeholder = String.raw`\[REDACTED(?:_[A-Z_]+)?\]`;
|
|
4959
5028
|
const trimmed = match.trim();
|
|
4960
|
-
return new RegExp(`^${
|
|
5029
|
+
return new RegExp(`^${REDACTION_PLACEHOLDER}$`).test(trimmed) || new RegExp(`=\\s*['"]?${REDACTION_PLACEHOLDER}['"]?$`).test(trimmed);
|
|
5030
|
+
}
|
|
5031
|
+
function isRedactionPlaceholderKey(key) {
|
|
5032
|
+
return new RegExp(`^${REDACTION_PLACEHOLDER}$`).test(key.trim());
|
|
4961
5033
|
}
|
|
4962
5034
|
function isSecretKey(key) {
|
|
5035
|
+
if (isRedactionPlaceholderKey(key))
|
|
5036
|
+
return false;
|
|
4963
5037
|
if (NON_SECRET_USAGE_KEYS.has(key.toLowerCase()))
|
|
4964
5038
|
return false;
|
|
4965
5039
|
if (DEFAULT_SECRET_KEY_PATTERN.test(key))
|
|
@@ -5020,7 +5094,7 @@ function upsertSecretSafetyConfig(input) {
|
|
|
5020
5094
|
saveConfig({ ...config, secret_safety: next });
|
|
5021
5095
|
return next;
|
|
5022
5096
|
}
|
|
5023
|
-
var DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS;
|
|
5097
|
+
var DEFAULT_SECRET_PATTERNS, DEFAULT_SECRET_KEY_PATTERN, NON_SECRET_USAGE_KEYS, REDACTION_PLACEHOLDER;
|
|
5024
5098
|
var init_redaction = __esm(() => {
|
|
5025
5099
|
init_config2();
|
|
5026
5100
|
DEFAULT_SECRET_PATTERNS = [
|
|
@@ -5044,6 +5118,7 @@ var init_redaction = __esm(() => {
|
|
|
5044
5118
|
"completion_tokens",
|
|
5045
5119
|
"cost_tokens"
|
|
5046
5120
|
]);
|
|
5121
|
+
REDACTION_PLACEHOLDER = String.raw`\[REDACTED(?:_[A-Z_]+)?\]`;
|
|
5047
5122
|
});
|
|
5048
5123
|
|
|
5049
5124
|
// src/lib/workspace-trust.ts
|
|
@@ -6683,7 +6758,7 @@ function scanTextForSecrets(text, options = {}) {
|
|
|
6683
6758
|
};
|
|
6684
6759
|
}
|
|
6685
6760
|
function redactText(text, options = {}) {
|
|
6686
|
-
const placeholder = options.placeholder ??
|
|
6761
|
+
const placeholder = options.placeholder ?? REDACTION_PLACEHOLDER2;
|
|
6687
6762
|
let out = text;
|
|
6688
6763
|
for (const { pattern, allowlist_ok } of DEFAULT_PATTERNS) {
|
|
6689
6764
|
out = out.replace(new RegExp(pattern.source, pattern.flags), (match) => {
|
|
@@ -6744,7 +6819,7 @@ function redactExportRecord(record) {
|
|
|
6744
6819
|
function getDefaultSecretPatterns() {
|
|
6745
6820
|
return DEFAULT_PATTERNS.map((p) => ({ name: p.name, source: p.pattern.source }));
|
|
6746
6821
|
}
|
|
6747
|
-
var SECRET_REDACTION_SCHEMA,
|
|
6822
|
+
var SECRET_REDACTION_SCHEMA, REDACTION_PLACEHOLDER2 = "[REDACTED]", DEFAULT_PATTERNS, DEFAULT_ALLOWLIST, customRedactors;
|
|
6748
6823
|
var init_secret_redaction = __esm(() => {
|
|
6749
6824
|
init_redaction();
|
|
6750
6825
|
SECRET_REDACTION_SCHEMA = ["todos", "secret_redaction", "v1"].join(".");
|
|
@@ -8801,10 +8876,11 @@ function getTaskWithRelations(id, db) {
|
|
|
8801
8876
|
JOIN task_dependencies td ON td.depends_on = t.id
|
|
8802
8877
|
WHERE td.task_id = ?`).all(id);
|
|
8803
8878
|
const dependencies = depRows.map(rowToTask);
|
|
8804
|
-
const
|
|
8879
|
+
const blocked_by = dependencies.filter((dep) => isBlockingDependencyStatus(dep.status));
|
|
8880
|
+
const blocksRows = d.query(`SELECT t.* FROM tasks t
|
|
8805
8881
|
JOIN task_dependencies td ON td.task_id = t.id
|
|
8806
8882
|
WHERE td.depends_on = ?`).all(id);
|
|
8807
|
-
const
|
|
8883
|
+
const blocks = blocksRows.map(rowToTask);
|
|
8808
8884
|
const comments = d.query("SELECT * FROM task_comments WHERE task_id = ? ORDER BY created_at").all(id);
|
|
8809
8885
|
const parent = task.parent_id ? getTask(task.parent_id, d) : null;
|
|
8810
8886
|
const checklist = getChecklist(id, d);
|
|
@@ -8813,6 +8889,7 @@ function getTaskWithRelations(id, db) {
|
|
|
8813
8889
|
subtasks,
|
|
8814
8890
|
dependencies,
|
|
8815
8891
|
blocked_by,
|
|
8892
|
+
blocks,
|
|
8816
8893
|
comments,
|
|
8817
8894
|
parent,
|
|
8818
8895
|
checklist
|
|
@@ -11936,7 +12013,7 @@ var init_dispatches = __esm(() => {
|
|
|
11936
12013
|
// package.json
|
|
11937
12014
|
var package_default = {
|
|
11938
12015
|
name: "@hasna/todos",
|
|
11939
|
-
version: "0.13.
|
|
12016
|
+
version: "0.13.3",
|
|
11940
12017
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
11941
12018
|
type: "module",
|
|
11942
12019
|
main: "dist/index.js",
|
|
@@ -11970,6 +12047,10 @@ var package_default = {
|
|
|
11970
12047
|
"./storage": {
|
|
11971
12048
|
types: "./dist/storage.d.ts",
|
|
11972
12049
|
import: "./dist/storage.js"
|
|
12050
|
+
},
|
|
12051
|
+
"./testing": {
|
|
12052
|
+
types: "./dist/testing.d.ts",
|
|
12053
|
+
import: "./dist/testing.js"
|
|
11973
12054
|
}
|
|
11974
12055
|
},
|
|
11975
12056
|
workspaces: [
|
|
@@ -11982,13 +12063,13 @@ var package_default = {
|
|
|
11982
12063
|
"README.md"
|
|
11983
12064
|
],
|
|
11984
12065
|
scripts: {
|
|
11985
|
-
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",
|
|
11986
|
-
"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/*'",
|
|
11987
12068
|
migrate: "bun run src/server/index.ts migrate",
|
|
11988
12069
|
"backfill:comment-redaction": "bun run src/server/index.ts redact-comments",
|
|
11989
12070
|
"generate:sdk": "bun run scripts/generate-sdk.ts",
|
|
11990
12071
|
"build:dashboard": "cd dashboard && bun install --frozen-lockfile && bun run build",
|
|
11991
|
-
typecheck: "tsc --noEmit",
|
|
12072
|
+
typecheck: "tsc --noEmit -p tsconfig.typecheck.json",
|
|
11992
12073
|
test: "bun test",
|
|
11993
12074
|
"test:no-cloud": "bun test src/no-cloud-boundary.test.ts src/local-first.test.ts src/lib/public-release-gate.test.ts",
|
|
11994
12075
|
"dev:cli": "bun run src/cli/index.tsx",
|
|
@@ -17730,11 +17811,12 @@ var DEFAULT_LIMITS = {
|
|
|
17730
17811
|
verification_limit: 10,
|
|
17731
17812
|
run_limit: 3,
|
|
17732
17813
|
dependency_limit: 12,
|
|
17733
|
-
plan_task_limit:
|
|
17814
|
+
plan_task_limit: 24,
|
|
17734
17815
|
max_text_chars: 6000,
|
|
17735
17816
|
summary_char_limit: 480,
|
|
17736
17817
|
stale_after_hours: 72
|
|
17737
17818
|
};
|
|
17819
|
+
var MAX_CONTEXT_TASKS = 24;
|
|
17738
17820
|
var ALL_CONTEXT_SECTIONS = [
|
|
17739
17821
|
"project",
|
|
17740
17822
|
"plan",
|
|
@@ -17767,7 +17849,7 @@ function limits(input) {
|
|
|
17767
17849
|
verification_limit: clamp(input.verification_limit, DEFAULT_LIMITS.verification_limit, 100),
|
|
17768
17850
|
run_limit: clamp(input.run_limit, DEFAULT_LIMITS.run_limit, 20),
|
|
17769
17851
|
dependency_limit: clamp(input.dependency_limit, DEFAULT_LIMITS.dependency_limit, 100),
|
|
17770
|
-
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),
|
|
17771
17853
|
max_text_chars: clamp(input.max_text_chars, DEFAULT_LIMITS.max_text_chars, 50000),
|
|
17772
17854
|
summary_char_limit: clamp(input.summary_char_limit, DEFAULT_LIMITS.summary_char_limit, 4000),
|
|
17773
17855
|
stale_after_hours: clamp(input.stale_after_hours, DEFAULT_LIMITS.stale_after_hours, 24 * 365)
|
|
@@ -17790,6 +17872,13 @@ function taskSummary(task2) {
|
|
|
17790
17872
|
priority: task2.priority
|
|
17791
17873
|
};
|
|
17792
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
|
+
}
|
|
17793
17882
|
function acceptanceCriteria(task2, maxText) {
|
|
17794
17883
|
const metadata = task2.metadata || {};
|
|
17795
17884
|
const raw = metadata["acceptance_criteria"] ?? metadata["acceptanceCriteria"] ?? metadata["criteria"];
|
|
@@ -17887,7 +17976,7 @@ function summarizeSection(pack, section, maxChars) {
|
|
|
17887
17976
|
return "No plan was attached.";
|
|
17888
17977
|
return summarizeStrings([
|
|
17889
17978
|
`Plan ${pack.plan.name} is ${pack.plan.status}`,
|
|
17890
|
-
`${pack.plan.tasks.length} listed plan
|
|
17979
|
+
`${pack.plan.tasks.length} of ${pack.plan.total_tasks} plan tasks listed; ${pack.plan.omitted_tasks} omitted`,
|
|
17891
17980
|
...pack.plan.tasks.slice(0, 4).map((task2) => `${task2.status} ${task2.short_id || task2.id.slice(0, 8)} ${task2.title}`)
|
|
17892
17981
|
], maxChars);
|
|
17893
17982
|
}
|
|
@@ -18037,7 +18126,8 @@ function createAgentContextPack(input, db) {
|
|
|
18037
18126
|
const limit = limits(input);
|
|
18038
18127
|
const project = task2.project_id ? getProject(task2.project_id, d) : null;
|
|
18039
18128
|
const plan = task2.plan_id ? getPlan(task2.plan_id, d) : null;
|
|
18040
|
-
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;
|
|
18041
18131
|
const upstream = getTaskDependencies(task2.id, d).map((dep) => taskSummary(getTask(dep.depends_on, d))).filter((item) => Boolean(item));
|
|
18042
18132
|
const downstream = getTaskDependents(task2.id, d).map((dep) => taskSummary(getTask(dep.task_id, d))).filter((item) => Boolean(item));
|
|
18043
18133
|
const comments = listComments(task2.id, d);
|
|
@@ -18080,6 +18170,9 @@ function createAgentContextPack(input, db) {
|
|
|
18080
18170
|
warnings.push(`${runs.length - selectedRuns.length} older runs omitted`);
|
|
18081
18171
|
if (fileMap.size > relevantFiles.length)
|
|
18082
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
|
+
}
|
|
18083
18176
|
const contextTask = {
|
|
18084
18177
|
id: task2.id,
|
|
18085
18178
|
short_id: task2.short_id,
|
|
@@ -18111,8 +18204,9 @@ function createAgentContextPack(input, db) {
|
|
|
18111
18204
|
description: truncate2(plan.description, limit.max_text_chars),
|
|
18112
18205
|
status: plan.status,
|
|
18113
18206
|
agent_id: plan.agent_id,
|
|
18114
|
-
tasks: planTasks.
|
|
18115
|
-
|
|
18207
|
+
tasks: planTasks.map(taskSummary).filter((item) => Boolean(item)),
|
|
18208
|
+
total_tasks: totalPlanTasks,
|
|
18209
|
+
omitted_tasks: Math.max(0, totalPlanTasks - planTasks.length)
|
|
18116
18210
|
} : null,
|
|
18117
18211
|
acceptance_criteria: acceptanceCriteria(task2, limit.max_text_chars),
|
|
18118
18212
|
dependencies: {
|
|
@@ -18222,6 +18316,8 @@ ${pack.task.description}` : null,
|
|
|
18222
18316
|
"## Project And Plan",
|
|
18223
18317
|
pack.project ? `- Project: ${pack.project.name} (${pack.project.path})` : "- Project: none",
|
|
18224
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,
|
|
18225
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,
|
|
18226
18322
|
"",
|
|
18227
18323
|
"## Acceptance Criteria",
|
|
@@ -19890,7 +19986,7 @@ function createReleaseCompatibilityReport(options = {}) {
|
|
|
19890
19986
|
"todos --help",
|
|
19891
19987
|
"todos-mcp --help",
|
|
19892
19988
|
"todos-serve --help",
|
|
19893
|
-
"todos doctor"
|
|
19989
|
+
"todos doctor --no-fail-on-findings"
|
|
19894
19990
|
],
|
|
19895
19991
|
smoke_tests: [
|
|
19896
19992
|
"command -v todos",
|
|
@@ -19898,7 +19994,7 @@ function createReleaseCompatibilityReport(options = {}) {
|
|
|
19898
19994
|
"command -v todos-serve",
|
|
19899
19995
|
"todos --version",
|
|
19900
19996
|
"todos --help",
|
|
19901
|
-
"todos doctor"
|
|
19997
|
+
"todos doctor --no-fail-on-findings"
|
|
19902
19998
|
],
|
|
19903
19999
|
rollback: [
|
|
19904
20000
|
"npm view @hasna/todos versions --json",
|
|
@@ -20392,12 +20488,11 @@ ${task2.description || ""}`.split(/\r?\n/);
|
|
|
20392
20488
|
function normalizeStackLocation(value) {
|
|
20393
20489
|
return value.replace(/:\d+(?::\d+)?/g, "").replace(/^file:\/\//, "").toLowerCase().trim();
|
|
20394
20490
|
}
|
|
20395
|
-
function
|
|
20491
|
+
function fingerprintContent(task2) {
|
|
20396
20492
|
const body = task2.description || "";
|
|
20397
20493
|
const text = `${task2.title}
|
|
20398
20494
|
${body}`;
|
|
20399
20495
|
return {
|
|
20400
|
-
task: task2,
|
|
20401
20496
|
title: normalizeText(task2.title),
|
|
20402
20497
|
body: normalizeText(body),
|
|
20403
20498
|
text: normalizeText(text),
|
|
@@ -20406,6 +20501,9 @@ ${body}`;
|
|
|
20406
20501
|
stackKeys: stackKeysFor(task2)
|
|
20407
20502
|
};
|
|
20408
20503
|
}
|
|
20504
|
+
function fingerprint(task2) {
|
|
20505
|
+
return { task: task2, ...fingerprintContent(task2) };
|
|
20506
|
+
}
|
|
20409
20507
|
function intersects(left, right) {
|
|
20410
20508
|
for (const value of left)
|
|
20411
20509
|
if (right.has(value))
|
|
@@ -23592,6 +23690,13 @@ var TODOS_PACKAGE_EXPORTS = [
|
|
|
23592
23690
|
types: "./dist/storage.d.ts",
|
|
23593
23691
|
description: "Storage and service adapter contracts for local SQLite implementations.",
|
|
23594
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"
|
|
23595
23700
|
}
|
|
23596
23701
|
];
|
|
23597
23702
|
function source8(version) {
|
|
@@ -23623,6 +23728,9 @@ var TODOS_REGISTRY = createTodosRegistry({
|
|
|
23623
23728
|
// src/storage/index.ts
|
|
23624
23729
|
init_config();
|
|
23625
23730
|
|
|
23731
|
+
// src/storage/local-sqlite.ts
|
|
23732
|
+
init_types();
|
|
23733
|
+
|
|
23626
23734
|
// src/lib/search.ts
|
|
23627
23735
|
init_database();
|
|
23628
23736
|
function rowToTask3(row) {
|
|
@@ -24439,6 +24547,267 @@ init_audit();
|
|
|
24439
24547
|
init_comments();
|
|
24440
24548
|
init_database();
|
|
24441
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
|
+
|
|
24442
24811
|
// src/storage/sqlite-snapshot.ts
|
|
24443
24812
|
init_database();
|
|
24444
24813
|
init_audit();
|
|
@@ -24830,15 +25199,15 @@ function resolveTaskRefLocal(db, ref) {
|
|
|
24830
25199
|
return null;
|
|
24831
25200
|
if (TASK_UUID_RE.test(raw))
|
|
24832
25201
|
return getTask(raw, db);
|
|
24833
|
-
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}`)}%`);
|
|
24834
25203
|
if (prefixRows.length > 1) {
|
|
24835
|
-
throw new
|
|
25204
|
+
throw new TaskReferenceAmbiguousError(ref, prefixRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
|
|
24836
25205
|
}
|
|
24837
25206
|
if (prefixRows.length === 1)
|
|
24838
25207
|
return getTask(prefixRows[0].id, db);
|
|
24839
|
-
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);
|
|
24840
25209
|
if (shortIdRows.length > 1) {
|
|
24841
|
-
throw new
|
|
25210
|
+
throw new TaskReferenceAmbiguousError(ref, shortIdRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
|
|
24842
25211
|
}
|
|
24843
25212
|
if (shortIdRows.length === 1)
|
|
24844
25213
|
return getTask(shortIdRows[0].id, db);
|
|
@@ -24987,6 +25356,9 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
24987
25356
|
exportSnapshot: () => exportSqliteTodosStorageSnapshot(database()),
|
|
24988
25357
|
importSnapshot: (snapshot) => importSqliteTodosStorageSnapshot(snapshot, database())
|
|
24989
25358
|
},
|
|
25359
|
+
integrity: {
|
|
25360
|
+
report: () => scanSqliteIntegrity(database())
|
|
25361
|
+
},
|
|
24990
25362
|
transaction: (fn) => {
|
|
24991
25363
|
const tx = database().transaction(() => fn(adapter));
|
|
24992
25364
|
return tx();
|
|
@@ -24995,6 +25367,10 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
24995
25367
|
return adapter;
|
|
24996
25368
|
}
|
|
24997
25369
|
|
|
25370
|
+
// src/storage/postgres-adapter.ts
|
|
25371
|
+
init_types();
|
|
25372
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
25373
|
+
|
|
24998
25374
|
// src/storage/postgres-sync.ts
|
|
24999
25375
|
var DEFAULT_TODOS_POSTGRES_SYNC_TABLE = "todos_sync_records";
|
|
25000
25376
|
var DEFAULT_TODOS_POSTGRES_CURSOR_TABLE = "todos_sync_cursors";
|
|
@@ -25384,54 +25760,7 @@ function assertSafeIdentifier(value) {
|
|
|
25384
25760
|
throw new Error(`Unsafe Postgres identifier: ${value}`);
|
|
25385
25761
|
}
|
|
25386
25762
|
|
|
25387
|
-
// src/storage/hybrid.ts
|
|
25388
|
-
function createHybridTodosStorageAdapter(options) {
|
|
25389
|
-
const local = options.localAdapter ?? createLocalSqliteTodosStorageAdapter(options.local);
|
|
25390
|
-
const syncStore = options.syncStore ?? (options.postgresClient ? createPostgresTodosSyncStore(options.postgresClient, { sourceMachineId: options.sourceMachineId }) : null);
|
|
25391
|
-
if (!syncStore)
|
|
25392
|
-
throw new Error("hybrid storage requires a Postgres sync store or query client");
|
|
25393
|
-
if (!local.sync.exportSnapshot || !local.sync.importSnapshot) {
|
|
25394
|
-
throw new Error("hybrid storage requires local snapshot export/import support");
|
|
25395
|
-
}
|
|
25396
|
-
const exportSnapshot = async (context) => await local.sync.exportSnapshot(context);
|
|
25397
|
-
const importSnapshot = async (snapshot, context) => await local.sync.importSnapshot(snapshot, context);
|
|
25398
|
-
const remote = {
|
|
25399
|
-
ensureSchema: () => syncStore.ensureSchema(),
|
|
25400
|
-
async pushSnapshot(context) {
|
|
25401
|
-
const snapshot = await exportSnapshot(context);
|
|
25402
|
-
return syncStore.pushSnapshot(snapshot, context);
|
|
25403
|
-
},
|
|
25404
|
-
async pullSnapshot(options2, context) {
|
|
25405
|
-
const snapshot = await syncStore.pullSnapshot(options2);
|
|
25406
|
-
return importSnapshot(snapshot, context);
|
|
25407
|
-
},
|
|
25408
|
-
async syncOnce(options2, context) {
|
|
25409
|
-
const pulled = await remote.pullSnapshot(options2, context);
|
|
25410
|
-
const pushed = await remote.pushSnapshot(context);
|
|
25411
|
-
return { pulled, pushed };
|
|
25412
|
-
}
|
|
25413
|
-
};
|
|
25414
|
-
return {
|
|
25415
|
-
...local,
|
|
25416
|
-
kind: "hybrid",
|
|
25417
|
-
capabilities: {
|
|
25418
|
-
...local.capabilities,
|
|
25419
|
-
localPersistence: true,
|
|
25420
|
-
remotePersistence: true,
|
|
25421
|
-
sync: true
|
|
25422
|
-
},
|
|
25423
|
-
sync: {
|
|
25424
|
-
...local.sync,
|
|
25425
|
-
exportSnapshot,
|
|
25426
|
-
importSnapshot
|
|
25427
|
-
},
|
|
25428
|
-
remote
|
|
25429
|
-
};
|
|
25430
|
-
}
|
|
25431
|
-
|
|
25432
25763
|
// src/storage/postgres-adapter.ts
|
|
25433
|
-
init_types();
|
|
25434
|
-
import { randomUUID as randomUUID3 } from "crypto";
|
|
25435
25764
|
init_redaction();
|
|
25436
25765
|
function createPostgresTodosStorageAdapter(options) {
|
|
25437
25766
|
const store = new PostgresJsonRecordStore(options);
|
|
@@ -25559,6 +25888,9 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
25559
25888
|
exportSnapshot: () => exportSnapshot(store),
|
|
25560
25889
|
importSnapshot: (snapshot, context) => importSnapshot(snapshot, store, context)
|
|
25561
25890
|
},
|
|
25891
|
+
integrity: {
|
|
25892
|
+
report: () => store.integrityReport()
|
|
25893
|
+
},
|
|
25562
25894
|
transaction: (fn) => fn(adapter)
|
|
25563
25895
|
};
|
|
25564
25896
|
return adapter;
|
|
@@ -25665,8 +25997,9 @@ class PostgresJsonRecordStore {
|
|
|
25665
25997
|
conds.push(`payload->>'agent_id' = ${p(filter.agent_id)}`);
|
|
25666
25998
|
if (filter.session_id !== undefined)
|
|
25667
25999
|
conds.push(`payload->>'session_id' = ${p(filter.session_id)}`);
|
|
25668
|
-
if (filter.tags?.length)
|
|
25669
|
-
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
|
+
}
|
|
25670
26003
|
if (filter.has_recurrence !== undefined) {
|
|
25671
26004
|
conds.push(`(COALESCE(payload->>'recurrence_rule', '') <> '') = ${p(filter.has_recurrence)}`);
|
|
25672
26005
|
}
|
|
@@ -25727,7 +26060,8 @@ class PostgresJsonRecordStore {
|
|
|
25727
26060
|
AND object_id COLLATE "C" >= $2 AND object_id COLLATE "C" < $3
|
|
25728
26061
|
LIMIT 2`, [this.service, raw, upper]);
|
|
25729
26062
|
if (prefixResult.rows.length > 1) {
|
|
25730
|
-
|
|
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 })));
|
|
25731
26065
|
}
|
|
25732
26066
|
if (prefixResult.rows.length === 1) {
|
|
25733
26067
|
return payloadRecord2(prefixResult.rows[0].payload);
|
|
@@ -25738,7 +26072,8 @@ class PostgresJsonRecordStore {
|
|
|
25738
26072
|
AND LOWER(payload->>'short_id') = $2
|
|
25739
26073
|
LIMIT 2`, [this.service, raw]);
|
|
25740
26074
|
if (shortIdResult.rows.length > 1) {
|
|
25741
|
-
|
|
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 })));
|
|
25742
26077
|
}
|
|
25743
26078
|
if (shortIdResult.rows.length === 1) {
|
|
25744
26079
|
return payloadRecord2(shortIdResult.rows[0].payload);
|
|
@@ -25752,6 +26087,24 @@ class PostgresJsonRecordStore {
|
|
|
25752
26087
|
const result = await this.options.client.query(sql, params);
|
|
25753
26088
|
return Number(result.rows[0]?.count ?? 0);
|
|
25754
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
|
+
}
|
|
25755
26108
|
async listTombstones() {
|
|
25756
26109
|
await this.ensureSchema();
|
|
25757
26110
|
const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
|
|
@@ -26227,7 +26580,7 @@ async function startTask2(id, agentId, store) {
|
|
|
26227
26580
|
if (task2.status !== "pending" && task2.status !== "in_progress") {
|
|
26228
26581
|
throw new Error(`Task is ${task2.status} and cannot be started by ${agentId}`);
|
|
26229
26582
|
}
|
|
26230
|
-
|
|
26583
|
+
const started = await patchTask(task2, {
|
|
26231
26584
|
status: "in_progress",
|
|
26232
26585
|
assigned_to: task2.assigned_to ?? agentId,
|
|
26233
26586
|
agent_id: task2.agent_id ?? agentId,
|
|
@@ -26235,6 +26588,8 @@ async function startTask2(id, agentId, store) {
|
|
|
26235
26588
|
locked_at: new Date().toISOString(),
|
|
26236
26589
|
started_at: task2.started_at ?? new Date().toISOString()
|
|
26237
26590
|
}, store);
|
|
26591
|
+
await logTaskChange2(task2.id, "start", "status", task2.status, "in_progress", agentId, store);
|
|
26592
|
+
return started;
|
|
26238
26593
|
}
|
|
26239
26594
|
async function completeTask2(id, agentId, options, store) {
|
|
26240
26595
|
const task2 = await store.completeTask(id, agentId, options);
|
|
@@ -26352,9 +26707,11 @@ async function removeDependency2(taskId, dependsOn, store) {
|
|
|
26352
26707
|
}
|
|
26353
26708
|
async function listDependencies(taskId, store) {
|
|
26354
26709
|
const edges = await store.list("dependencies");
|
|
26710
|
+
const incoming = edges.filter((edge) => edge.depends_on === taskId).map((edge) => ({ task_id: edge.task_id, depends_on: edge.depends_on }));
|
|
26355
26711
|
return {
|
|
26356
26712
|
dependencies: edges.filter((edge) => edge.task_id === taskId).map((edge) => ({ task_id: edge.task_id, depends_on: edge.depends_on })),
|
|
26357
|
-
|
|
26713
|
+
blocks: incoming,
|
|
26714
|
+
blocked_by: incoming
|
|
26358
26715
|
};
|
|
26359
26716
|
}
|
|
26360
26717
|
async function addVerification(input, store, context) {
|
|
@@ -27341,17 +27698,17 @@ function createTodosCloudQueryClientFromEnv(env = process.env, options = {}) {
|
|
|
27341
27698
|
init_config();
|
|
27342
27699
|
function createTodosStorageAdapter(options = {}) {
|
|
27343
27700
|
const config = options.config ?? loadTodosStorageConfig(options.env);
|
|
27344
|
-
if (config.mode === "
|
|
27701
|
+
if (config.mode === "sqlite") {
|
|
27345
27702
|
if (isTodosShadowEnabled(options.env))
|
|
27346
27703
|
return createShadowAdapter(options, config);
|
|
27347
27704
|
return createLocalSqliteTodosStorageAdapter(options.local);
|
|
27348
27705
|
}
|
|
27349
27706
|
assertTodosRemoteStorageConfig(config);
|
|
27350
|
-
const adapter =
|
|
27707
|
+
const adapter = options.remoteAdapter ?? createImplicitPostgresAdapter(options);
|
|
27351
27708
|
if (!adapter) {
|
|
27352
|
-
throw new Error(
|
|
27709
|
+
throw new Error("postgres storage requires a repo-native Postgres adapter. " + "Pass remoteAdapter or postgresClient after wiring Postgres RDS support.");
|
|
27353
27710
|
}
|
|
27354
|
-
|
|
27711
|
+
assertPostgresAdapterCapabilities(adapter);
|
|
27355
27712
|
return adapter;
|
|
27356
27713
|
}
|
|
27357
27714
|
function createShadowAdapter(options, config) {
|
|
@@ -27365,7 +27722,7 @@ function createShadowAdapter(options, config) {
|
|
|
27365
27722
|
...options.local ? { local: options.local } : {},
|
|
27366
27723
|
...options.postgresSyncStore ? { syncStore: options.postgresSyncStore } : {},
|
|
27367
27724
|
...postgresClient ? { postgresClient } : {},
|
|
27368
|
-
...options.
|
|
27725
|
+
...options.sourceMachineId ? { sourceMachineId: options.sourceMachineId } : {}
|
|
27369
27726
|
});
|
|
27370
27727
|
}
|
|
27371
27728
|
function createImplicitPostgresAdapter(options) {
|
|
@@ -27373,25 +27730,12 @@ function createImplicitPostgresAdapter(options) {
|
|
|
27373
27730
|
return null;
|
|
27374
27731
|
return createPostgresTodosStorageAdapter({
|
|
27375
27732
|
client: options.postgresClient,
|
|
27376
|
-
...options.
|
|
27377
|
-
});
|
|
27378
|
-
}
|
|
27379
|
-
function createImplicitHybridAdapter(options) {
|
|
27380
|
-
if (!options.postgresClient && !options.postgresSyncStore)
|
|
27381
|
-
return null;
|
|
27382
|
-
return createHybridTodosStorageAdapter({
|
|
27383
|
-
...options.hybrid ?? {},
|
|
27384
|
-
local: options.local,
|
|
27385
|
-
postgresClient: options.postgresClient,
|
|
27386
|
-
syncStore: options.postgresSyncStore
|
|
27733
|
+
...options.sourceMachineId ? { sourceMachineId: options.sourceMachineId } : {}
|
|
27387
27734
|
});
|
|
27388
27735
|
}
|
|
27389
|
-
function
|
|
27736
|
+
function assertPostgresAdapterCapabilities(adapter) {
|
|
27390
27737
|
if (!adapter.capabilities.remotePersistence) {
|
|
27391
|
-
throw new Error(
|
|
27392
|
-
}
|
|
27393
|
-
if (mode === "hybrid" && !adapter.capabilities.localPersistence) {
|
|
27394
|
-
throw new Error("hybrid storage adapter must also set capabilities.localPersistence=true");
|
|
27738
|
+
throw new Error("postgres storage adapter must set capabilities.remotePersistence=true");
|
|
27395
27739
|
}
|
|
27396
27740
|
}
|
|
27397
27741
|
// src/storage/shadow-outbox.ts
|
|
@@ -27718,6 +28062,50 @@ async function closeRuntimeShadowCloud() {
|
|
|
27718
28062
|
await cloud.close().catch(() => {});
|
|
27719
28063
|
}
|
|
27720
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
|
+
}
|
|
27721
28109
|
// src/storage/comment-redaction-backfill.ts
|
|
27722
28110
|
init_redaction();
|
|
27723
28111
|
var COMMENT_REDACTION_BACKFILL_CONFIRMATION = "REDACT_STORED_TODOS_COMMENTS";
|
|
@@ -31296,10 +31684,10 @@ function getNativeStorageStatus(env = process.env) {
|
|
|
31296
31684
|
} catch (error) {
|
|
31297
31685
|
issues.push(error instanceof Error ? error.message : String(error));
|
|
31298
31686
|
}
|
|
31299
|
-
const remoteEnabled =
|
|
31687
|
+
const remoteEnabled = isTodosPostgresBackend(config);
|
|
31300
31688
|
const remoteFieldsConfigured = Boolean(config.database || config.objectStorage);
|
|
31301
31689
|
if (!remoteEnabled && remoteFieldsConfigured) {
|
|
31302
|
-
warnings.push(
|
|
31690
|
+
warnings.push(`the sqlite backend (${TODOS_STORAGE_ENV.mode}) ignores configured remote storage fields`);
|
|
31303
31691
|
}
|
|
31304
31692
|
if (remoteEnabled && !config.objectStorage) {
|
|
31305
31693
|
warnings.push(`${TODOS_STORAGE_ENV.s3Bucket} is not configured, so artifact sync will stay local`);
|
|
@@ -31308,7 +31696,7 @@ function getNativeStorageStatus(env = process.env) {
|
|
|
31308
31696
|
ok: issues.length === 0,
|
|
31309
31697
|
service: "todos",
|
|
31310
31698
|
mode: config.mode,
|
|
31311
|
-
local_default: config.mode === "
|
|
31699
|
+
local_default: config.mode === "sqlite",
|
|
31312
31700
|
remote_enabled: remoteEnabled,
|
|
31313
31701
|
database: {
|
|
31314
31702
|
configured: Boolean(config.database),
|
|
@@ -31390,8 +31778,12 @@ function storageEnvStatus(env) {
|
|
|
31390
31778
|
]));
|
|
31391
31779
|
}
|
|
31392
31780
|
function fallbackConfig(env) {
|
|
31393
|
-
|
|
31394
|
-
|
|
31781
|
+
let mode;
|
|
31782
|
+
try {
|
|
31783
|
+
mode = parseStorageBackend(clean2(env[TODOS_STORAGE_ENV.mode]));
|
|
31784
|
+
} catch {
|
|
31785
|
+
mode = "sqlite";
|
|
31786
|
+
}
|
|
31395
31787
|
return {
|
|
31396
31788
|
service: "todos",
|
|
31397
31789
|
mode,
|
|
@@ -41733,6 +42125,8 @@ init_database();
|
|
|
41733
42125
|
init_tasks();
|
|
41734
42126
|
init_task_graph();
|
|
41735
42127
|
init_task_lifecycle();
|
|
42128
|
+
init_types();
|
|
42129
|
+
init_types();
|
|
41736
42130
|
var DEPENDENCY_GRAPH_SCHEMA = "todos.dependency_graph.v1";
|
|
41737
42131
|
var TASK_DEPENDENCY_EDGES_SCHEMA = "todos.task_dependency_edges.v1";
|
|
41738
42132
|
var PROJECT_DEPENDENCY_GRAPH_SCHEMA = "todos.project_dependency_graph.v1";
|
|
@@ -41988,13 +42382,14 @@ function getBlockers(taskId, db) {
|
|
|
41988
42382
|
missing_dependencies: missing
|
|
41989
42383
|
};
|
|
41990
42384
|
}
|
|
41991
|
-
function buildTaskDependencyEdges(task2, dependencies,
|
|
42385
|
+
function buildTaskDependencyEdges(task2, dependencies, blocks) {
|
|
41992
42386
|
return {
|
|
41993
42387
|
schema_version: TASK_DEPENDENCY_EDGES_SCHEMA,
|
|
41994
42388
|
task_id: task2.id,
|
|
41995
42389
|
short_id: task2.short_id,
|
|
41996
42390
|
dependencies: dependencies.map(toNode),
|
|
41997
|
-
blocked_by:
|
|
42391
|
+
blocked_by: dependencies.filter((dep) => isBlockingDependencyStatus(dep.status)).map(toNode),
|
|
42392
|
+
blocks: blocks.map(toNode)
|
|
41998
42393
|
};
|
|
41999
42394
|
}
|
|
42000
42395
|
function getTaskDependencyEdges(taskId, db) {
|
|
@@ -42003,8 +42398,8 @@ function getTaskDependencyEdges(taskId, db) {
|
|
|
42003
42398
|
if (!task2)
|
|
42004
42399
|
return null;
|
|
42005
42400
|
const dependencies = getTaskDependencies(taskId, d).map((edge) => getTask(edge.depends_on, d)).filter((t) => Boolean(t));
|
|
42006
|
-
const
|
|
42007
|
-
return buildTaskDependencyEdges(task2, dependencies,
|
|
42401
|
+
const blocks = getTaskDependents(taskId, d).map((edge) => getTask(edge.task_id, d)).filter((t) => Boolean(t));
|
|
42402
|
+
return buildTaskDependencyEdges(task2, dependencies, blocks);
|
|
42008
42403
|
}
|
|
42009
42404
|
function buildProjectDependencyGraph(projectId, tasks, edges, generatedAt = new Date().toISOString()) {
|
|
42010
42405
|
const nodeIds = new Set(tasks.map((t) => t.id));
|
|
@@ -50567,7 +50962,7 @@ function parseObject4(value) {
|
|
|
50567
50962
|
function normalizeKey(value) {
|
|
50568
50963
|
return value.trim().toLowerCase().replace(/[^a-z0-9._:/-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
50569
50964
|
}
|
|
50570
|
-
function
|
|
50965
|
+
function normalizeTaskFindingFingerprint(value) {
|
|
50571
50966
|
const normalized = normalizeKey(value);
|
|
50572
50967
|
if (!normalized)
|
|
50573
50968
|
throw new Error("finding fingerprint is required");
|
|
@@ -50676,7 +51071,7 @@ function assertTask(taskId, db) {
|
|
|
50676
51071
|
throw new TaskNotFoundError(taskId);
|
|
50677
51072
|
}
|
|
50678
51073
|
function nextFinding(input, db) {
|
|
50679
|
-
const fingerprint4 =
|
|
51074
|
+
const fingerprint4 = normalizeTaskFindingFingerprint(input.fingerprint);
|
|
50680
51075
|
const title = redactOptional(input.title, 300);
|
|
50681
51076
|
if (!title)
|
|
50682
51077
|
throw new Error("finding title is required");
|
|
@@ -50824,7 +51219,7 @@ function resolveMissingTaskFindings(input, db) {
|
|
|
50824
51219
|
const timestamp3 = input.resolved_at || now();
|
|
50825
51220
|
const status = normalizeResolutionStatus(input.status);
|
|
50826
51221
|
const runId = resolveRunForTask(input.run_id, input.task_id, d);
|
|
50827
|
-
const present = new Set(input.fingerprints.map(
|
|
51222
|
+
const present = new Set(input.fingerprints.map(normalizeTaskFindingFingerprint));
|
|
50828
51223
|
const warnings = [];
|
|
50829
51224
|
const conditions = ["task_id = ?", "status = 'open'"];
|
|
50830
51225
|
const params = [input.task_id];
|
|
@@ -51351,12 +51746,12 @@ init_artifact_store();
|
|
|
51351
51746
|
|
|
51352
51747
|
// src/lib/doctor.ts
|
|
51353
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";
|
|
51354
51751
|
init_migrations();
|
|
51355
51752
|
init_schema();
|
|
51356
51753
|
init_recurrence();
|
|
51357
|
-
|
|
51358
|
-
import { basename as basename7, dirname as dirname18, join as join27 } from "path";
|
|
51359
|
-
var REQUIRED_TABLES2 = [
|
|
51754
|
+
var REQUIRED_TABLES3 = [
|
|
51360
51755
|
"_migrations",
|
|
51361
51756
|
"projects",
|
|
51362
51757
|
"tasks",
|
|
@@ -51369,7 +51764,7 @@ var REQUIRED_TABLES2 = [
|
|
|
51369
51764
|
"task_run_commands",
|
|
51370
51765
|
"task_run_artifacts"
|
|
51371
51766
|
];
|
|
51372
|
-
function
|
|
51767
|
+
function tableExists2(db, table) {
|
|
51373
51768
|
return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
|
|
51374
51769
|
}
|
|
51375
51770
|
function quoteIdent(identifier) {
|
|
@@ -51453,7 +51848,7 @@ function findDuplicateIndexes(db) {
|
|
|
51453
51848
|
return duplicates;
|
|
51454
51849
|
}
|
|
51455
51850
|
function findMissingProjectRoots(db) {
|
|
51456
|
-
if (!
|
|
51851
|
+
if (!tableExists2(db, "projects"))
|
|
51457
51852
|
return 0;
|
|
51458
51853
|
let missing = 0;
|
|
51459
51854
|
const rows = db.query("SELECT path FROM projects WHERE path IS NOT NULL AND path != ''").all();
|
|
@@ -51468,7 +51863,7 @@ function findMissingProjectRoots(db) {
|
|
|
51468
51863
|
return missing;
|
|
51469
51864
|
}
|
|
51470
51865
|
function addTaskStateChecks(db, checks) {
|
|
51471
|
-
if (!
|
|
51866
|
+
if (!tableExists2(db, "tasks"))
|
|
51472
51867
|
return;
|
|
51473
51868
|
const columns = getTableColumns(db, "tasks");
|
|
51474
51869
|
const staleCutoff = new Date(Date.now() - 30 * 60 * 1000).toISOString();
|
|
@@ -51540,15 +51935,42 @@ function createBackup(dbPath) {
|
|
|
51540
51935
|
function pushRepair(repairs, type, message, applied, count3) {
|
|
51541
51936
|
repairs.push({ type, message, applied, count: count3 });
|
|
51542
51937
|
}
|
|
51543
|
-
function summarize3(checks, repairs) {
|
|
51938
|
+
function summarize3(checks, repairs, integrity) {
|
|
51544
51939
|
return {
|
|
51545
51940
|
errors: checks.filter((check) => check.severity === "error").length,
|
|
51546
51941
|
warnings: checks.filter((check) => check.severity === "warn").length,
|
|
51547
51942
|
infos: checks.filter((check) => check.severity === "info").length,
|
|
51548
51943
|
repairable: checks.filter((check) => check.repairable).length,
|
|
51549
|
-
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
|
|
51550
51948
|
};
|
|
51551
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
|
+
}
|
|
51552
51974
|
function deleteOrphans(db, table, where) {
|
|
51553
51975
|
const before = countQuery2(db, `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`);
|
|
51554
51976
|
if (before > 0)
|
|
@@ -51577,7 +51999,7 @@ function runTodosDoctor(options = {}) {
|
|
|
51577
51999
|
message: `Schema at migration ${migrationCurrent}`
|
|
51578
52000
|
});
|
|
51579
52001
|
}
|
|
51580
|
-
const missingTables =
|
|
52002
|
+
const missingTables = REQUIRED_TABLES3.filter((table) => !tableExists2(db, table));
|
|
51581
52003
|
if (missingTables.length > 0) {
|
|
51582
52004
|
addCheck(checks, {
|
|
51583
52005
|
severity: "error",
|
|
@@ -51587,7 +52009,7 @@ function runTodosDoctor(options = {}) {
|
|
|
51587
52009
|
repairable: true
|
|
51588
52010
|
});
|
|
51589
52011
|
}
|
|
51590
|
-
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;
|
|
51591
52013
|
if (orphanedParents > 0) {
|
|
51592
52014
|
addCheck(checks, {
|
|
51593
52015
|
severity: "error",
|
|
@@ -51597,7 +52019,7 @@ function runTodosDoctor(options = {}) {
|
|
|
51597
52019
|
repairable: true
|
|
51598
52020
|
});
|
|
51599
52021
|
}
|
|
51600
|
-
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;
|
|
51601
52023
|
if (orphanedDependencies > 0) {
|
|
51602
52024
|
addCheck(checks, {
|
|
51603
52025
|
severity: "error",
|
|
@@ -51616,7 +52038,7 @@ function runTodosDoctor(options = {}) {
|
|
|
51616
52038
|
];
|
|
51617
52039
|
let orphanedRows2 = 0;
|
|
51618
52040
|
for (const [table, where] of orphanTables) {
|
|
51619
|
-
if (!
|
|
52041
|
+
if (!tableExists2(db, table))
|
|
51620
52042
|
continue;
|
|
51621
52043
|
orphanedRows2 += countQuery2(db, `SELECT COUNT(*) as count FROM ${table} WHERE ${where}`);
|
|
51622
52044
|
}
|
|
@@ -51630,6 +52052,8 @@ function runTodosDoctor(options = {}) {
|
|
|
51630
52052
|
});
|
|
51631
52053
|
}
|
|
51632
52054
|
addTaskStateChecks(db, checks);
|
|
52055
|
+
const integrity = scanSqliteIntegrity(db);
|
|
52056
|
+
addIntegrityChecks(integrity, checks);
|
|
51633
52057
|
const corruptJson = findCorruptJsonMetadata(db);
|
|
51634
52058
|
if (corruptJson.length > 0) {
|
|
51635
52059
|
addCheck(checks, {
|
|
@@ -51688,12 +52112,12 @@ function runTodosDoctor(options = {}) {
|
|
|
51688
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)");
|
|
51689
52113
|
pushRepair(repairs, "orphaned_task_parents", "Cleared missing parent references", true, orphanedParents);
|
|
51690
52114
|
}
|
|
51691
|
-
if (orphanedDependencies > 0 &&
|
|
52115
|
+
if (orphanedDependencies > 0 && tableExists2(db, "task_dependencies")) {
|
|
51692
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)");
|
|
51693
52117
|
pushRepair(repairs, "orphaned_task_dependencies", "Deleted dependency rows referencing missing tasks", true, count3);
|
|
51694
52118
|
}
|
|
51695
52119
|
for (const [table, where] of orphanTables) {
|
|
51696
|
-
if (!
|
|
52120
|
+
if (!tableExists2(db, table))
|
|
51697
52121
|
continue;
|
|
51698
52122
|
const count3 = deleteOrphans(db, table, where);
|
|
51699
52123
|
if (count3 > 0)
|
|
@@ -51722,16 +52146,19 @@ function runTodosDoctor(options = {}) {
|
|
|
51722
52146
|
}
|
|
51723
52147
|
}
|
|
51724
52148
|
}
|
|
51725
|
-
const
|
|
51726
|
-
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);
|
|
51727
52153
|
return {
|
|
51728
|
-
ok: !finalChecks.some((check) => check.severity === "error"),
|
|
52154
|
+
ok: !finalChecks.some((check) => check.severity === "error") && finalIntegrity.summary.ok,
|
|
51729
52155
|
dry_run: !apply,
|
|
51730
52156
|
database_path: dbPath,
|
|
51731
52157
|
migration: { current: getMigrationLevel(db), expected: migrationExpected },
|
|
51732
52158
|
backup,
|
|
51733
52159
|
checks: finalChecks,
|
|
51734
52160
|
repairs,
|
|
52161
|
+
integrity: finalIntegrity,
|
|
51735
52162
|
summary
|
|
51736
52163
|
};
|
|
51737
52164
|
}
|
|
@@ -52277,6 +52704,7 @@ export {
|
|
|
52277
52704
|
updateArtifactRedaction,
|
|
52278
52705
|
updateAgentActivity,
|
|
52279
52706
|
updateAgent,
|
|
52707
|
+
unverifiedCondition,
|
|
52280
52708
|
unsubscribeResource,
|
|
52281
52709
|
unlockTask,
|
|
52282
52710
|
unlinkTaskCommit,
|
|
@@ -52297,6 +52725,7 @@ export {
|
|
|
52297
52725
|
summarizeTaskRun,
|
|
52298
52726
|
summarizeRoadmap,
|
|
52299
52727
|
summarizeMilestone,
|
|
52728
|
+
summarizeIntegrity,
|
|
52300
52729
|
suggestAgentNames,
|
|
52301
52730
|
subscribeResource,
|
|
52302
52731
|
storeArtifactContent,
|
|
@@ -52337,6 +52766,7 @@ export {
|
|
|
52337
52766
|
scorePlanHealth,
|
|
52338
52767
|
scheduleTask,
|
|
52339
52768
|
scanTextForSecrets,
|
|
52769
|
+
scanSqliteIntegrity,
|
|
52340
52770
|
scanSourceForForbiddenWebPatterns,
|
|
52341
52771
|
scanReminders,
|
|
52342
52772
|
scanPreWriteText,
|
|
@@ -52384,6 +52814,7 @@ export {
|
|
|
52384
52814
|
resolvePartialId,
|
|
52385
52815
|
resolveMissingTaskFindings,
|
|
52386
52816
|
resolveMentions,
|
|
52817
|
+
resolveIntegritySeverity,
|
|
52387
52818
|
resolveGitRoot,
|
|
52388
52819
|
resolveCommandQuery,
|
|
52389
52820
|
resolveAccessProfile,
|
|
@@ -52498,6 +52929,7 @@ export {
|
|
|
52498
52929
|
patrolTasks,
|
|
52499
52930
|
parseTmuxTarget,
|
|
52500
52931
|
parseStorageMode,
|
|
52932
|
+
parseStorageBackend,
|
|
52501
52933
|
parseRecurrenceRule,
|
|
52502
52934
|
parseQuietHours,
|
|
52503
52935
|
parsePlanArtifactMarkdown,
|
|
@@ -52526,6 +52958,8 @@ export {
|
|
|
52526
52958
|
migrationDryRun,
|
|
52527
52959
|
migrateWorkflowStates,
|
|
52528
52960
|
mergeDuplicateTask,
|
|
52961
|
+
measuredCondition,
|
|
52962
|
+
measureIntegrityRows,
|
|
52529
52963
|
materializePlanSteps,
|
|
52530
52964
|
matchCapabilities,
|
|
52531
52965
|
looksSensitiveKey,
|
|
@@ -52644,12 +53078,14 @@ export {
|
|
|
52644
53078
|
isWorktreePath,
|
|
52645
53079
|
isValidRecurrenceRule,
|
|
52646
53080
|
isTodosRemoteStorageEnabled,
|
|
53081
|
+
isTodosPostgresBackend,
|
|
52647
53082
|
isSnapshotStale,
|
|
52648
53083
|
isEncryptedValue,
|
|
52649
53084
|
isEncryptedBridgeBundle,
|
|
52650
53085
|
isArtifactExpired,
|
|
52651
53086
|
isAllowedLocalApiUrl,
|
|
52652
53087
|
isAgentConflict,
|
|
53088
|
+
integrityCondition,
|
|
52653
53089
|
installTemplateLibrary,
|
|
52654
53090
|
installLocalExtension,
|
|
52655
53091
|
inspectPlanArtifact,
|
|
@@ -52692,6 +53128,7 @@ export {
|
|
|
52692
53128
|
getTodosStorageEnvName,
|
|
52693
53129
|
getTodosStorageDatabaseUrl,
|
|
52694
53130
|
getTodosStorageDatabaseEnv,
|
|
53131
|
+
getTodosStorageBackend,
|
|
52695
53132
|
getTimeReport,
|
|
52696
53133
|
getTimeLogs,
|
|
52697
53134
|
getTerminalNotificationRule,
|
|
@@ -52899,6 +53336,7 @@ export {
|
|
|
52899
53336
|
formatLockConflict,
|
|
52900
53337
|
formatKnowledgeSnapshotMarkdown,
|
|
52901
53338
|
formatIssueImportPreviewText,
|
|
53339
|
+
formatIntegrityMessage,
|
|
52902
53340
|
formatIntakePreviewText,
|
|
52903
53341
|
formatHandoffPacket,
|
|
52904
53342
|
formatGoalHandoff,
|
|
@@ -53129,16 +53567,19 @@ export {
|
|
|
53129
53567
|
bulkAddTaskFiles,
|
|
53130
53568
|
buildTaskDependencyEdges,
|
|
53131
53569
|
buildTaskBoardSnapshot,
|
|
53570
|
+
buildSqliteIntegritySql,
|
|
53132
53571
|
buildS3ObjectUrl,
|
|
53133
53572
|
buildS3ObjectKey,
|
|
53134
53573
|
buildRunReplayBundle,
|
|
53135
53574
|
buildResourceSnapshot,
|
|
53136
53575
|
buildReportExportData,
|
|
53137
53576
|
buildProjectDependencyGraph,
|
|
53577
|
+
buildPostgresIntegritySql,
|
|
53138
53578
|
buildPlanArtifactSnapshot,
|
|
53139
53579
|
buildMcpToolGroups,
|
|
53140
53580
|
buildMachineTopologyReport,
|
|
53141
53581
|
buildKnowledgeSnapshotPayload,
|
|
53582
|
+
buildIntegrityReport,
|
|
53142
53583
|
buildHandoffPacket,
|
|
53143
53584
|
buildFeatureManifest,
|
|
53144
53585
|
buildFailureTriageReport,
|
|
@@ -53173,6 +53614,7 @@ export {
|
|
|
53173
53614
|
appendRunCommand,
|
|
53174
53615
|
analyzeDependencyGraph,
|
|
53175
53616
|
agentClaimNextSafe,
|
|
53617
|
+
adoptRemoteIntegrityReport,
|
|
53176
53618
|
addTemplateTasks,
|
|
53177
53619
|
addTaskRunFile,
|
|
53178
53620
|
addTaskRunEvent,
|
|
@@ -53219,6 +53661,7 @@ export {
|
|
|
53219
53661
|
TODOS_LOCAL_BACKUP_KIND,
|
|
53220
53662
|
TODOS_JSON_CONTRACTS_MANIFEST,
|
|
53221
53663
|
TODOS_JSON_CONTRACTS,
|
|
53664
|
+
TODOS_INTEGRITY_SCHEMA_VERSION,
|
|
53222
53665
|
TODOS_ERROR_CODES,
|
|
53223
53666
|
TODOS_ENCRYPTION_SCHEMA_VERSION,
|
|
53224
53667
|
TODOS_ENCRYPTED_VALUE_KIND,
|
|
@@ -53261,7 +53704,7 @@ export {
|
|
|
53261
53704
|
REMINDER_STATUSES,
|
|
53262
53705
|
RELEASE_CHECK_SCHEMA,
|
|
53263
53706
|
RELATIONSHIP_TYPES,
|
|
53264
|
-
REDACTION_PLACEHOLDER,
|
|
53707
|
+
REDACTION_PLACEHOLDER2 as REDACTION_PLACEHOLDER,
|
|
53265
53708
|
ProjectNotFoundError,
|
|
53266
53709
|
PreWriteSecretError,
|
|
53267
53710
|
PrGroupLedgerError,
|
|
@@ -53275,6 +53718,7 @@ export {
|
|
|
53275
53718
|
PLAN_EXECUTION_SCHEMA,
|
|
53276
53719
|
PLAN_EXECUTION_MODES,
|
|
53277
53720
|
PARITY_SCHEMA_VERSION,
|
|
53721
|
+
OPEN_TASK_STATUSES,
|
|
53278
53722
|
NOTIFICATION_REMINDERS_SCHEMA,
|
|
53279
53723
|
NL_INTAKE_SCHEMA,
|
|
53280
53724
|
NESTED_SUBCOMMANDS,
|
|
@@ -53303,6 +53747,7 @@ export {
|
|
|
53303
53747
|
JSON_OUTPUT_CONTRACT,
|
|
53304
53748
|
ISSUE_SOURCES,
|
|
53305
53749
|
ISSUE_IMPORT_SCHEMA,
|
|
53750
|
+
INTEGRITY_CONDITIONS,
|
|
53306
53751
|
INTAKE_TRIAGE_STATUSES,
|
|
53307
53752
|
INTAKE_SOURCE_TYPES,
|
|
53308
53753
|
INBOX_INTAKE_SCHEMA,
|