@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/storage.js
CHANGED
|
@@ -43,15 +43,18 @@ var __require = import.meta.require;
|
|
|
43
43
|
var exports_config = {};
|
|
44
44
|
__export(exports_config, {
|
|
45
45
|
parseStorageMode: () => parseStorageMode,
|
|
46
|
+
parseStorageBackend: () => parseStorageBackend,
|
|
46
47
|
loadTodosStorageConfig: () => loadTodosStorageConfig,
|
|
47
48
|
loadStorageConfig: () => loadStorageConfig,
|
|
48
49
|
isTodosShadowEnabled: () => isTodosShadowEnabled,
|
|
49
50
|
isTodosRemoteStorageEnabled: () => isTodosRemoteStorageEnabled,
|
|
51
|
+
isTodosPostgresBackend: () => isTodosPostgresBackend,
|
|
50
52
|
getTodosStorageShadowEnvName: () => getTodosStorageShadowEnvName,
|
|
51
53
|
getTodosStorageMode: () => getTodosStorageMode,
|
|
52
54
|
getTodosStorageEnvName: () => getTodosStorageEnvName,
|
|
53
55
|
getTodosStorageDatabaseUrl: () => getTodosStorageDatabaseUrl,
|
|
54
56
|
getTodosStorageDatabaseEnv: () => getTodosStorageDatabaseEnv,
|
|
57
|
+
getTodosStorageBackend: () => getTodosStorageBackend,
|
|
55
58
|
getStorageMode: () => getStorageMode,
|
|
56
59
|
getStorageDatabaseUrl: () => getStorageDatabaseUrl,
|
|
57
60
|
getStorageDatabaseEnv: () => getStorageDatabaseEnv,
|
|
@@ -76,7 +79,7 @@ function getCanonicalTodosRdsConfig(env = process.env) {
|
|
|
76
79
|
};
|
|
77
80
|
}
|
|
78
81
|
function loadTodosStorageConfig(env = process.env) {
|
|
79
|
-
const mode =
|
|
82
|
+
const mode = getTodosStorageBackend(env);
|
|
80
83
|
const databaseUrl = getTodosStorageDatabaseUrl(env);
|
|
81
84
|
const bucket = readStorageEnv(env, "s3Bucket").value;
|
|
82
85
|
const prefix = readStorageEnv(env, "s3Prefix").value ?? "todos/";
|
|
@@ -113,29 +116,41 @@ function loadTodosStorageConfig(env = process.env) {
|
|
|
113
116
|
function loadStorageConfig(env = process.env) {
|
|
114
117
|
return loadTodosStorageConfig(env);
|
|
115
118
|
}
|
|
119
|
+
function isTodosPostgresBackend(config) {
|
|
120
|
+
return config.mode === "postgres";
|
|
121
|
+
}
|
|
116
122
|
function isTodosRemoteStorageEnabled(config) {
|
|
117
|
-
return config
|
|
123
|
+
return isTodosPostgresBackend(config);
|
|
118
124
|
}
|
|
119
125
|
function assertTodosRemoteStorageConfig(config) {
|
|
120
|
-
if (!
|
|
126
|
+
if (!isTodosPostgresBackend(config))
|
|
121
127
|
return;
|
|
122
128
|
if (!config.database?.url) {
|
|
123
129
|
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${TODOS_STORAGE_ENV.mode}=${config.mode}`);
|
|
124
130
|
}
|
|
125
131
|
}
|
|
126
|
-
function
|
|
132
|
+
function parseStorageBackend(value) {
|
|
127
133
|
const normalized = clean(value)?.toLowerCase();
|
|
128
134
|
if (!normalized)
|
|
129
|
-
return "
|
|
130
|
-
if (normalized === "
|
|
135
|
+
return "sqlite";
|
|
136
|
+
if (normalized === "sqlite" || normalized === "postgres")
|
|
131
137
|
return normalized;
|
|
132
|
-
|
|
138
|
+
const legacy = LEGACY_BACKEND_TOKENS[normalized];
|
|
139
|
+
if (legacy)
|
|
140
|
+
return legacy;
|
|
141
|
+
throw new Error(`${TODOS_STORAGE_ENV.mode} must be sqlite or postgres (legacy values local and remote are accepted)`);
|
|
142
|
+
}
|
|
143
|
+
function parseStorageMode(value) {
|
|
144
|
+
return parseStorageBackend(value);
|
|
145
|
+
}
|
|
146
|
+
function getTodosStorageBackend(env = process.env) {
|
|
147
|
+
return parseStorageBackend(readStorageEnv(env, "mode").value);
|
|
133
148
|
}
|
|
134
149
|
function getTodosStorageMode(env = process.env) {
|
|
135
|
-
return
|
|
150
|
+
return getTodosStorageBackend(env);
|
|
136
151
|
}
|
|
137
152
|
function getStorageMode(env = process.env) {
|
|
138
|
-
return
|
|
153
|
+
return getTodosStorageBackend(env);
|
|
139
154
|
}
|
|
140
155
|
function isTodosShadowEnabled(env = process.env) {
|
|
141
156
|
return parseBoolean(readStorageEnv(env, "shadow").value, false);
|
|
@@ -146,8 +161,8 @@ function getTodosStorageShadowEnvName(env = process.env) {
|
|
|
146
161
|
function assertTodosShadowConfig(config, env = process.env) {
|
|
147
162
|
if (!isTodosShadowEnabled(env))
|
|
148
163
|
return;
|
|
149
|
-
if (config.mode !== "
|
|
150
|
-
throw new Error(`${readStorageEnv(env, "shadow").name} shadow mirror requires
|
|
164
|
+
if (config.mode !== "sqlite") {
|
|
165
|
+
throw new Error(`${readStorageEnv(env, "shadow").name} shadow mirror requires the sqlite backend (got ${config.mode})`);
|
|
151
166
|
}
|
|
152
167
|
if (!config.database?.url) {
|
|
153
168
|
throw new Error(`${TODOS_STORAGE_ENV.databaseUrl} is required when ${readStorageEnv(env, "shadow").name} is enabled`);
|
|
@@ -203,7 +218,7 @@ function parsePositiveInteger(value, fallback) {
|
|
|
203
218
|
}
|
|
204
219
|
return parsed;
|
|
205
220
|
}
|
|
206
|
-
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";
|
|
221
|
+
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;
|
|
207
222
|
var init_config = __esm(() => {
|
|
208
223
|
TODOS_STORAGE_TABLES = [
|
|
209
224
|
"todos_sync_records",
|
|
@@ -244,6 +259,187 @@ var init_config = __esm(() => {
|
|
|
244
259
|
syncBatchSize: "TODOS_SYNC_BATCH_SIZE",
|
|
245
260
|
syncDryRun: "TODOS_SYNC_DRY_RUN"
|
|
246
261
|
};
|
|
262
|
+
LEGACY_BACKEND_TOKENS = {
|
|
263
|
+
local: "sqlite",
|
|
264
|
+
remote: "postgres",
|
|
265
|
+
postgresql: "postgres",
|
|
266
|
+
hybrid: "postgres",
|
|
267
|
+
self_hosted: "postgres",
|
|
268
|
+
cloud: "postgres"
|
|
269
|
+
};
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// src/types/index.ts
|
|
273
|
+
function isBlockingDependencyStatus(status) {
|
|
274
|
+
return status !== "completed" && status !== "cancelled";
|
|
275
|
+
}
|
|
276
|
+
var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
|
|
277
|
+
var init_types = __esm(() => {
|
|
278
|
+
TASK_STATUSES = [
|
|
279
|
+
"pending",
|
|
280
|
+
"in_progress",
|
|
281
|
+
"completed",
|
|
282
|
+
"failed",
|
|
283
|
+
"cancelled"
|
|
284
|
+
];
|
|
285
|
+
TASK_PRIORITIES = [
|
|
286
|
+
"low",
|
|
287
|
+
"medium",
|
|
288
|
+
"high",
|
|
289
|
+
"critical"
|
|
290
|
+
];
|
|
291
|
+
PLAN_STATUSES = ["active", "completed", "archived"];
|
|
292
|
+
VersionConflictError = class VersionConflictError extends Error {
|
|
293
|
+
taskId;
|
|
294
|
+
expectedVersion;
|
|
295
|
+
actualVersion;
|
|
296
|
+
static code = "VERSION_CONFLICT";
|
|
297
|
+
static suggestion = "Fetch the task with get_task to get the current version before updating.";
|
|
298
|
+
constructor(taskId, expectedVersion, actualVersion) {
|
|
299
|
+
super(`Version conflict for task ${taskId}: expected ${expectedVersion}, got ${actualVersion}`);
|
|
300
|
+
this.taskId = taskId;
|
|
301
|
+
this.expectedVersion = expectedVersion;
|
|
302
|
+
this.actualVersion = actualVersion;
|
|
303
|
+
this.name = "VersionConflictError";
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
TaskNotFoundError = class TaskNotFoundError extends Error {
|
|
307
|
+
taskId;
|
|
308
|
+
static code = "TASK_NOT_FOUND";
|
|
309
|
+
static suggestion = "Verify the task ID. Use list_tasks or search_tasks to find the correct ID.";
|
|
310
|
+
constructor(taskId) {
|
|
311
|
+
super(`Task not found: ${taskId}`);
|
|
312
|
+
this.taskId = taskId;
|
|
313
|
+
this.name = "TaskNotFoundError";
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
TaskReferenceAmbiguousError = class TaskReferenceAmbiguousError extends Error {
|
|
317
|
+
reference;
|
|
318
|
+
static code = "TASK_REFERENCE_AMBIGUOUS";
|
|
319
|
+
candidateTaskIds;
|
|
320
|
+
candidateProjectIds;
|
|
321
|
+
constructor(reference, candidates) {
|
|
322
|
+
const candidateTaskIds = [...new Set(candidates.map((candidate) => candidate.task_id))].sort();
|
|
323
|
+
const candidateProjectIds = [...new Set(candidates.map((candidate) => candidate.project_id).filter((projectId) => typeof projectId === "string" && projectId.length > 0))].sort();
|
|
324
|
+
const projectList = candidateProjectIds.length > 0 ? candidateProjectIds.join(", ") : "(unscoped)";
|
|
325
|
+
super(`Task reference is ambiguous: "${reference}". Candidate project IDs: ${projectList}. ` + "Use a full task UUID.");
|
|
326
|
+
this.reference = reference;
|
|
327
|
+
this.name = "TaskReferenceAmbiguousError";
|
|
328
|
+
this.candidateTaskIds = candidateTaskIds;
|
|
329
|
+
this.candidateProjectIds = candidateProjectIds;
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
ProjectNotFoundError = class ProjectNotFoundError extends Error {
|
|
333
|
+
projectId;
|
|
334
|
+
static code = "PROJECT_NOT_FOUND";
|
|
335
|
+
static suggestion = "Use list_projects to see available projects.";
|
|
336
|
+
constructor(projectId) {
|
|
337
|
+
super(`Project not found: ${projectId}`);
|
|
338
|
+
this.projectId = projectId;
|
|
339
|
+
this.name = "ProjectNotFoundError";
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
ResourceConflictError = class ResourceConflictError extends Error {
|
|
343
|
+
code;
|
|
344
|
+
constructor(code, message) {
|
|
345
|
+
super(message);
|
|
346
|
+
this.code = code;
|
|
347
|
+
this.name = "ResourceConflictError";
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
PlanNotFoundError = class PlanNotFoundError extends Error {
|
|
351
|
+
planId;
|
|
352
|
+
static code = "PLAN_NOT_FOUND";
|
|
353
|
+
static suggestion = "Use list_plans to see available plans.";
|
|
354
|
+
constructor(planId) {
|
|
355
|
+
super(`Plan not found: ${planId}`);
|
|
356
|
+
this.planId = planId;
|
|
357
|
+
this.name = "PlanNotFoundError";
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
LockError = class LockError extends Error {
|
|
361
|
+
taskId;
|
|
362
|
+
lockedBy;
|
|
363
|
+
static code = "LOCK_ERROR";
|
|
364
|
+
static suggestion = "Wait for the lock to expire (30 min) or contact the lock holder.";
|
|
365
|
+
constructor(taskId, lockedBy) {
|
|
366
|
+
super(`Task ${taskId} is locked by ${lockedBy}`);
|
|
367
|
+
this.taskId = taskId;
|
|
368
|
+
this.lockedBy = lockedBy;
|
|
369
|
+
this.name = "LockError";
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
AgentNotFoundError = class AgentNotFoundError extends Error {
|
|
373
|
+
agentId;
|
|
374
|
+
static code = "AGENT_NOT_FOUND";
|
|
375
|
+
static suggestion = "Use register_agent to create the agent first, or list_agents to find existing ones.";
|
|
376
|
+
constructor(agentId) {
|
|
377
|
+
super(`Agent not found: ${agentId}`);
|
|
378
|
+
this.agentId = agentId;
|
|
379
|
+
this.name = "AgentNotFoundError";
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
IdentityAliasAmbiguousError = class IdentityAliasAmbiguousError extends Error {
|
|
383
|
+
code = "IDENTITY_ALIAS_AMBIGUOUS";
|
|
384
|
+
candidates;
|
|
385
|
+
constructor(subject, candidates = []) {
|
|
386
|
+
super(`Identity alias or source is ambiguous: ${subject}`);
|
|
387
|
+
this.name = "IdentityAliasAmbiguousError";
|
|
388
|
+
this.candidates = [...new Set(candidates)].sort();
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
IdentityIdImmutableError = class IdentityIdImmutableError extends Error {
|
|
392
|
+
code = "IDENTITY_ID_IMMUTABLE";
|
|
393
|
+
constructor(agentId) {
|
|
394
|
+
super(`IDENTITY_ID_IMMUTABLE: canonical identity for agent ${agentId} cannot be replaced`);
|
|
395
|
+
this.name = "IdentityIdImmutableError";
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
TaskListNotFoundError = class TaskListNotFoundError extends Error {
|
|
399
|
+
taskListId;
|
|
400
|
+
static code = "TASK_LIST_NOT_FOUND";
|
|
401
|
+
static suggestion = "Use list_task_lists to see available lists.";
|
|
402
|
+
constructor(taskListId) {
|
|
403
|
+
super(`Task list not found: ${taskListId}`);
|
|
404
|
+
this.taskListId = taskListId;
|
|
405
|
+
this.name = "TaskListNotFoundError";
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
DependencyCycleError = class DependencyCycleError extends Error {
|
|
409
|
+
taskId;
|
|
410
|
+
dependsOn;
|
|
411
|
+
static code = "DEPENDENCY_CYCLE";
|
|
412
|
+
static suggestion = "Check the dependency chain with get_task to avoid circular references.";
|
|
413
|
+
constructor(taskId, dependsOn) {
|
|
414
|
+
super(`Adding dependency ${taskId} -> ${dependsOn} would create a cycle`);
|
|
415
|
+
this.taskId = taskId;
|
|
416
|
+
this.dependsOn = dependsOn;
|
|
417
|
+
this.name = "DependencyCycleError";
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
CompletionGuardError = class CompletionGuardError extends Error {
|
|
421
|
+
reason;
|
|
422
|
+
retryAfterSeconds;
|
|
423
|
+
static code = "COMPLETION_BLOCKED";
|
|
424
|
+
static suggestion = "Wait for the cooldown period, then retry.";
|
|
425
|
+
constructor(reason, retryAfterSeconds) {
|
|
426
|
+
super(reason);
|
|
427
|
+
this.reason = reason;
|
|
428
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
429
|
+
this.name = "CompletionGuardError";
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
DISPATCH_STATUSES = ["pending", "sent", "failed", "cancelled"];
|
|
433
|
+
DispatchNotFoundError = class DispatchNotFoundError extends Error {
|
|
434
|
+
dispatchId;
|
|
435
|
+
static code = "DISPATCH_NOT_FOUND";
|
|
436
|
+
static suggestion = "Check the dispatch ID with list_dispatches.";
|
|
437
|
+
constructor(dispatchId) {
|
|
438
|
+
super(`Dispatch not found: ${dispatchId}`);
|
|
439
|
+
this.dispatchId = dispatchId;
|
|
440
|
+
this.name = "DispatchNotFoundError";
|
|
441
|
+
}
|
|
442
|
+
};
|
|
247
443
|
});
|
|
248
444
|
|
|
249
445
|
// src/db/migrations.ts
|
|
@@ -3630,160 +3826,6 @@ var init_machines = __esm(() => {
|
|
|
3630
3826
|
];
|
|
3631
3827
|
});
|
|
3632
3828
|
|
|
3633
|
-
// src/types/index.ts
|
|
3634
|
-
var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
|
|
3635
|
-
var init_types = __esm(() => {
|
|
3636
|
-
TASK_STATUSES = [
|
|
3637
|
-
"pending",
|
|
3638
|
-
"in_progress",
|
|
3639
|
-
"completed",
|
|
3640
|
-
"failed",
|
|
3641
|
-
"cancelled"
|
|
3642
|
-
];
|
|
3643
|
-
TASK_PRIORITIES = [
|
|
3644
|
-
"low",
|
|
3645
|
-
"medium",
|
|
3646
|
-
"high",
|
|
3647
|
-
"critical"
|
|
3648
|
-
];
|
|
3649
|
-
PLAN_STATUSES = ["active", "completed", "archived"];
|
|
3650
|
-
VersionConflictError = class VersionConflictError extends Error {
|
|
3651
|
-
taskId;
|
|
3652
|
-
expectedVersion;
|
|
3653
|
-
actualVersion;
|
|
3654
|
-
static code = "VERSION_CONFLICT";
|
|
3655
|
-
static suggestion = "Fetch the task with get_task to get the current version before updating.";
|
|
3656
|
-
constructor(taskId, expectedVersion, actualVersion) {
|
|
3657
|
-
super(`Version conflict for task ${taskId}: expected ${expectedVersion}, got ${actualVersion}`);
|
|
3658
|
-
this.taskId = taskId;
|
|
3659
|
-
this.expectedVersion = expectedVersion;
|
|
3660
|
-
this.actualVersion = actualVersion;
|
|
3661
|
-
this.name = "VersionConflictError";
|
|
3662
|
-
}
|
|
3663
|
-
};
|
|
3664
|
-
TaskNotFoundError = class TaskNotFoundError extends Error {
|
|
3665
|
-
taskId;
|
|
3666
|
-
static code = "TASK_NOT_FOUND";
|
|
3667
|
-
static suggestion = "Verify the task ID. Use list_tasks or search_tasks to find the correct ID.";
|
|
3668
|
-
constructor(taskId) {
|
|
3669
|
-
super(`Task not found: ${taskId}`);
|
|
3670
|
-
this.taskId = taskId;
|
|
3671
|
-
this.name = "TaskNotFoundError";
|
|
3672
|
-
}
|
|
3673
|
-
};
|
|
3674
|
-
ProjectNotFoundError = class ProjectNotFoundError extends Error {
|
|
3675
|
-
projectId;
|
|
3676
|
-
static code = "PROJECT_NOT_FOUND";
|
|
3677
|
-
static suggestion = "Use list_projects to see available projects.";
|
|
3678
|
-
constructor(projectId) {
|
|
3679
|
-
super(`Project not found: ${projectId}`);
|
|
3680
|
-
this.projectId = projectId;
|
|
3681
|
-
this.name = "ProjectNotFoundError";
|
|
3682
|
-
}
|
|
3683
|
-
};
|
|
3684
|
-
ResourceConflictError = class ResourceConflictError extends Error {
|
|
3685
|
-
code;
|
|
3686
|
-
constructor(code, message) {
|
|
3687
|
-
super(message);
|
|
3688
|
-
this.code = code;
|
|
3689
|
-
this.name = "ResourceConflictError";
|
|
3690
|
-
}
|
|
3691
|
-
};
|
|
3692
|
-
PlanNotFoundError = class PlanNotFoundError extends Error {
|
|
3693
|
-
planId;
|
|
3694
|
-
static code = "PLAN_NOT_FOUND";
|
|
3695
|
-
static suggestion = "Use list_plans to see available plans.";
|
|
3696
|
-
constructor(planId) {
|
|
3697
|
-
super(`Plan not found: ${planId}`);
|
|
3698
|
-
this.planId = planId;
|
|
3699
|
-
this.name = "PlanNotFoundError";
|
|
3700
|
-
}
|
|
3701
|
-
};
|
|
3702
|
-
LockError = class LockError extends Error {
|
|
3703
|
-
taskId;
|
|
3704
|
-
lockedBy;
|
|
3705
|
-
static code = "LOCK_ERROR";
|
|
3706
|
-
static suggestion = "Wait for the lock to expire (30 min) or contact the lock holder.";
|
|
3707
|
-
constructor(taskId, lockedBy) {
|
|
3708
|
-
super(`Task ${taskId} is locked by ${lockedBy}`);
|
|
3709
|
-
this.taskId = taskId;
|
|
3710
|
-
this.lockedBy = lockedBy;
|
|
3711
|
-
this.name = "LockError";
|
|
3712
|
-
}
|
|
3713
|
-
};
|
|
3714
|
-
AgentNotFoundError = class AgentNotFoundError extends Error {
|
|
3715
|
-
agentId;
|
|
3716
|
-
static code = "AGENT_NOT_FOUND";
|
|
3717
|
-
static suggestion = "Use register_agent to create the agent first, or list_agents to find existing ones.";
|
|
3718
|
-
constructor(agentId) {
|
|
3719
|
-
super(`Agent not found: ${agentId}`);
|
|
3720
|
-
this.agentId = agentId;
|
|
3721
|
-
this.name = "AgentNotFoundError";
|
|
3722
|
-
}
|
|
3723
|
-
};
|
|
3724
|
-
IdentityAliasAmbiguousError = class IdentityAliasAmbiguousError extends Error {
|
|
3725
|
-
code = "IDENTITY_ALIAS_AMBIGUOUS";
|
|
3726
|
-
candidates;
|
|
3727
|
-
constructor(subject, candidates = []) {
|
|
3728
|
-
super(`Identity alias or source is ambiguous: ${subject}`);
|
|
3729
|
-
this.name = "IdentityAliasAmbiguousError";
|
|
3730
|
-
this.candidates = [...new Set(candidates)].sort();
|
|
3731
|
-
}
|
|
3732
|
-
};
|
|
3733
|
-
IdentityIdImmutableError = class IdentityIdImmutableError extends Error {
|
|
3734
|
-
code = "IDENTITY_ID_IMMUTABLE";
|
|
3735
|
-
constructor(agentId) {
|
|
3736
|
-
super(`IDENTITY_ID_IMMUTABLE: canonical identity for agent ${agentId} cannot be replaced`);
|
|
3737
|
-
this.name = "IdentityIdImmutableError";
|
|
3738
|
-
}
|
|
3739
|
-
};
|
|
3740
|
-
TaskListNotFoundError = class TaskListNotFoundError extends Error {
|
|
3741
|
-
taskListId;
|
|
3742
|
-
static code = "TASK_LIST_NOT_FOUND";
|
|
3743
|
-
static suggestion = "Use list_task_lists to see available lists.";
|
|
3744
|
-
constructor(taskListId) {
|
|
3745
|
-
super(`Task list not found: ${taskListId}`);
|
|
3746
|
-
this.taskListId = taskListId;
|
|
3747
|
-
this.name = "TaskListNotFoundError";
|
|
3748
|
-
}
|
|
3749
|
-
};
|
|
3750
|
-
DependencyCycleError = class DependencyCycleError extends Error {
|
|
3751
|
-
taskId;
|
|
3752
|
-
dependsOn;
|
|
3753
|
-
static code = "DEPENDENCY_CYCLE";
|
|
3754
|
-
static suggestion = "Check the dependency chain with get_task to avoid circular references.";
|
|
3755
|
-
constructor(taskId, dependsOn) {
|
|
3756
|
-
super(`Adding dependency ${taskId} -> ${dependsOn} would create a cycle`);
|
|
3757
|
-
this.taskId = taskId;
|
|
3758
|
-
this.dependsOn = dependsOn;
|
|
3759
|
-
this.name = "DependencyCycleError";
|
|
3760
|
-
}
|
|
3761
|
-
};
|
|
3762
|
-
CompletionGuardError = class CompletionGuardError extends Error {
|
|
3763
|
-
reason;
|
|
3764
|
-
retryAfterSeconds;
|
|
3765
|
-
static code = "COMPLETION_BLOCKED";
|
|
3766
|
-
static suggestion = "Wait for the cooldown period, then retry.";
|
|
3767
|
-
constructor(reason, retryAfterSeconds) {
|
|
3768
|
-
super(reason);
|
|
3769
|
-
this.reason = reason;
|
|
3770
|
-
this.retryAfterSeconds = retryAfterSeconds;
|
|
3771
|
-
this.name = "CompletionGuardError";
|
|
3772
|
-
}
|
|
3773
|
-
};
|
|
3774
|
-
DISPATCH_STATUSES = ["pending", "sent", "failed", "cancelled"];
|
|
3775
|
-
DispatchNotFoundError = class DispatchNotFoundError extends Error {
|
|
3776
|
-
dispatchId;
|
|
3777
|
-
static code = "DISPATCH_NOT_FOUND";
|
|
3778
|
-
static suggestion = "Check the dispatch ID with list_dispatches.";
|
|
3779
|
-
constructor(dispatchId) {
|
|
3780
|
-
super(`Dispatch not found: ${dispatchId}`);
|
|
3781
|
-
this.dispatchId = dispatchId;
|
|
3782
|
-
this.name = "DispatchNotFoundError";
|
|
3783
|
-
}
|
|
3784
|
-
};
|
|
3785
|
-
});
|
|
3786
|
-
|
|
3787
3829
|
// src/db/identity-mapping.ts
|
|
3788
3830
|
function timestamp() {
|
|
3789
3831
|
return new Date().toISOString();
|
|
@@ -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(rowToTask2);
|
|
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(rowToTask2);
|
|
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
|
|
@@ -11824,6 +11901,9 @@ var init_tasks = __esm(() => {
|
|
|
11824
11901
|
// src/storage/index.ts
|
|
11825
11902
|
init_config();
|
|
11826
11903
|
|
|
11904
|
+
// src/storage/local-sqlite.ts
|
|
11905
|
+
init_types();
|
|
11906
|
+
|
|
11827
11907
|
// src/lib/search.ts
|
|
11828
11908
|
init_database();
|
|
11829
11909
|
function rowToTask(row) {
|
|
@@ -12640,6 +12720,267 @@ init_audit();
|
|
|
12640
12720
|
init_comments();
|
|
12641
12721
|
init_database();
|
|
12642
12722
|
|
|
12723
|
+
// src/db/integrity.ts
|
|
12724
|
+
init_database();
|
|
12725
|
+
|
|
12726
|
+
// src/lib/integrity.ts
|
|
12727
|
+
init_types();
|
|
12728
|
+
var TODOS_INTEGRITY_SCHEMA_VERSION = "todos.integrity.v1";
|
|
12729
|
+
var TERMINAL_TASK_STATUSES = ["completed", "failed", "cancelled"];
|
|
12730
|
+
var OPEN_TASK_STATUSES = TASK_STATUSES.filter((status) => !TERMINAL_TASK_STATUSES.includes(status));
|
|
12731
|
+
var INTEGRITY_CONDITIONS = [
|
|
12732
|
+
{
|
|
12733
|
+
id: "tasks_without_project",
|
|
12734
|
+
entity: "task",
|
|
12735
|
+
field: "project_id",
|
|
12736
|
+
target: "project",
|
|
12737
|
+
kind: "missing",
|
|
12738
|
+
base_severity: "warn",
|
|
12739
|
+
escalate_when_open: true,
|
|
12740
|
+
impact: "invisible to every project-scoped read (list, status, next, claim)"
|
|
12741
|
+
},
|
|
12742
|
+
{
|
|
12743
|
+
id: "tasks_without_task_list",
|
|
12744
|
+
entity: "task",
|
|
12745
|
+
field: "task_list_id",
|
|
12746
|
+
target: "task_list",
|
|
12747
|
+
kind: "missing",
|
|
12748
|
+
base_severity: "warn",
|
|
12749
|
+
escalate_when_open: true,
|
|
12750
|
+
impact: "invisible to every task-list read \u2014 the list reports zero open work"
|
|
12751
|
+
},
|
|
12752
|
+
{
|
|
12753
|
+
id: "tasks_with_unregistered_project",
|
|
12754
|
+
entity: "task",
|
|
12755
|
+
field: "project_id",
|
|
12756
|
+
target: "project",
|
|
12757
|
+
kind: "dangling",
|
|
12758
|
+
base_severity: "error",
|
|
12759
|
+
escalate_when_open: false,
|
|
12760
|
+
impact: "points at a project id that does not exist; the reference can never resolve"
|
|
12761
|
+
},
|
|
12762
|
+
{
|
|
12763
|
+
id: "tasks_with_unregistered_task_list",
|
|
12764
|
+
entity: "task",
|
|
12765
|
+
field: "task_list_id",
|
|
12766
|
+
target: "task_list",
|
|
12767
|
+
kind: "dangling",
|
|
12768
|
+
base_severity: "error",
|
|
12769
|
+
escalate_when_open: false,
|
|
12770
|
+
impact: "points at a task-list id that does not exist; the reference can never resolve"
|
|
12771
|
+
},
|
|
12772
|
+
{
|
|
12773
|
+
id: "task_lists_without_project",
|
|
12774
|
+
entity: "task_list",
|
|
12775
|
+
field: "project_id",
|
|
12776
|
+
target: "project",
|
|
12777
|
+
kind: "missing",
|
|
12778
|
+
base_severity: "warn",
|
|
12779
|
+
escalate_when_open: false,
|
|
12780
|
+
impact: "unbound list \u2014 unreachable from any project, so its tasks are unroutable"
|
|
12781
|
+
},
|
|
12782
|
+
{
|
|
12783
|
+
id: "task_lists_with_unregistered_project",
|
|
12784
|
+
entity: "task_list",
|
|
12785
|
+
field: "project_id",
|
|
12786
|
+
target: "project",
|
|
12787
|
+
kind: "dangling",
|
|
12788
|
+
base_severity: "error",
|
|
12789
|
+
escalate_when_open: false,
|
|
12790
|
+
impact: "points at a project id that does not exist; the list can never be reached"
|
|
12791
|
+
}
|
|
12792
|
+
];
|
|
12793
|
+
function integrityCondition(id) {
|
|
12794
|
+
const spec = INTEGRITY_CONDITIONS.find((condition) => condition.id === id);
|
|
12795
|
+
if (!spec)
|
|
12796
|
+
throw new Error(`unknown integrity condition: ${id}`);
|
|
12797
|
+
return spec;
|
|
12798
|
+
}
|
|
12799
|
+
function resolveIntegritySeverity(spec, measurement) {
|
|
12800
|
+
if (spec.kind === "dangling")
|
|
12801
|
+
return "error";
|
|
12802
|
+
if (spec.escalate_when_open && (measurement.open_count ?? 0) > 0)
|
|
12803
|
+
return "error";
|
|
12804
|
+
return spec.base_severity;
|
|
12805
|
+
}
|
|
12806
|
+
var ENTITY_LABEL = {
|
|
12807
|
+
task: ["task", "tasks"],
|
|
12808
|
+
task_list: ["task list", "task lists"]
|
|
12809
|
+
};
|
|
12810
|
+
function plural(entity, count) {
|
|
12811
|
+
const [one, many] = ENTITY_LABEL[entity];
|
|
12812
|
+
return count === 1 ? one : many;
|
|
12813
|
+
}
|
|
12814
|
+
function formatIntegrityMessage(spec, measurement) {
|
|
12815
|
+
const noun = plural(spec.entity, measurement.count);
|
|
12816
|
+
const verb = measurement.count === 1 ? ["has", "references"] : ["have", "reference"];
|
|
12817
|
+
const open = measurement.open_count === null || measurement.open_count === 0 ? "" : ` (${measurement.open_count} still open)`;
|
|
12818
|
+
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}`;
|
|
12819
|
+
}
|
|
12820
|
+
function measuredCondition(spec, measurement, source) {
|
|
12821
|
+
return {
|
|
12822
|
+
id: spec.id,
|
|
12823
|
+
entity: spec.entity,
|
|
12824
|
+
field: spec.field,
|
|
12825
|
+
kind: spec.kind,
|
|
12826
|
+
count: measurement.count,
|
|
12827
|
+
open_count: measurement.open_count,
|
|
12828
|
+
severity: measurement.count > 0 ? resolveIntegritySeverity(spec, measurement) : null,
|
|
12829
|
+
verified: true,
|
|
12830
|
+
source,
|
|
12831
|
+
message: formatIntegrityMessage(spec, measurement),
|
|
12832
|
+
impact: spec.impact
|
|
12833
|
+
};
|
|
12834
|
+
}
|
|
12835
|
+
function unverifiedCondition(spec, reason) {
|
|
12836
|
+
return {
|
|
12837
|
+
id: spec.id,
|
|
12838
|
+
entity: spec.entity,
|
|
12839
|
+
field: spec.field,
|
|
12840
|
+
kind: spec.kind,
|
|
12841
|
+
count: null,
|
|
12842
|
+
open_count: null,
|
|
12843
|
+
severity: null,
|
|
12844
|
+
verified: false,
|
|
12845
|
+
source: "unverified",
|
|
12846
|
+
unverified_reason: reason,
|
|
12847
|
+
message: `${spec.id}: NOT CHECKED \u2014 ${reason}`,
|
|
12848
|
+
impact: spec.impact
|
|
12849
|
+
};
|
|
12850
|
+
}
|
|
12851
|
+
function summarizeIntegrity(conditions) {
|
|
12852
|
+
const measured = conditions.filter((condition) => condition.verified && condition.count !== null);
|
|
12853
|
+
const findings = measured.filter((condition) => (condition.count ?? 0) > 0);
|
|
12854
|
+
const unverified = conditions.length - measured.length;
|
|
12855
|
+
return {
|
|
12856
|
+
ok: unverified === 0 && findings.length === 0,
|
|
12857
|
+
findings: findings.length,
|
|
12858
|
+
rows: findings.reduce((total, condition) => total + (condition.count ?? 0), 0),
|
|
12859
|
+
errors: findings.filter((condition) => condition.severity === "error").length,
|
|
12860
|
+
warnings: findings.filter((condition) => condition.severity === "warn").length,
|
|
12861
|
+
unverified,
|
|
12862
|
+
complete: unverified === 0
|
|
12863
|
+
};
|
|
12864
|
+
}
|
|
12865
|
+
function buildIntegrityReport(conditions, generatedAt) {
|
|
12866
|
+
const measuredSources = [...new Set(conditions.map((condition) => condition.source))].filter((source) => source !== "unverified");
|
|
12867
|
+
return {
|
|
12868
|
+
schema_version: TODOS_INTEGRITY_SCHEMA_VERSION,
|
|
12869
|
+
generated_at: generatedAt,
|
|
12870
|
+
source: measuredSources.length === 0 ? "unverified" : measuredSources.length === 1 ? measuredSources[0] : "remote-derived",
|
|
12871
|
+
conditions,
|
|
12872
|
+
summary: summarizeIntegrity(conditions)
|
|
12873
|
+
};
|
|
12874
|
+
}
|
|
12875
|
+
function adoptRemoteIntegrityReport(raw, fallbackGeneratedAt) {
|
|
12876
|
+
const received = new Map;
|
|
12877
|
+
if (Array.isArray(raw.conditions)) {
|
|
12878
|
+
for (const entry of raw.conditions) {
|
|
12879
|
+
if (entry && typeof entry["id"] === "string")
|
|
12880
|
+
received.set(entry["id"], entry);
|
|
12881
|
+
}
|
|
12882
|
+
}
|
|
12883
|
+
const conditions = INTEGRITY_CONDITIONS.map((spec) => {
|
|
12884
|
+
const entry = received.get(spec.id);
|
|
12885
|
+
const count = entry?.["count"];
|
|
12886
|
+
const verified = entry?.["verified"];
|
|
12887
|
+
if (!entry || verified === false || typeof count !== "number" || !Number.isFinite(count)) {
|
|
12888
|
+
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");
|
|
12889
|
+
}
|
|
12890
|
+
const openRaw = entry["open_count"];
|
|
12891
|
+
return measuredCondition(spec, {
|
|
12892
|
+
count,
|
|
12893
|
+
open_count: spec.entity === "task" ? typeof openRaw === "number" && Number.isFinite(openRaw) ? openRaw : 0 : null
|
|
12894
|
+
}, typeof raw.source === "string" && raw.source === "postgres" ? "postgres" : "remote-authority");
|
|
12895
|
+
});
|
|
12896
|
+
const report = buildIntegrityReport(conditions, typeof raw.generated_at === "string" ? raw.generated_at : fallbackGeneratedAt);
|
|
12897
|
+
return typeof raw.source === "string" && (raw.source === "sqlite" || raw.source === "postgres") ? { ...report, source: raw.source } : report;
|
|
12898
|
+
}
|
|
12899
|
+
var SQLITE_TABLE = {
|
|
12900
|
+
task: "tasks",
|
|
12901
|
+
task_list: "task_lists",
|
|
12902
|
+
project: "projects"
|
|
12903
|
+
};
|
|
12904
|
+
function sqliteOpenStatusList() {
|
|
12905
|
+
return OPEN_TASK_STATUSES.map((status) => `'${status}'`).join(", ");
|
|
12906
|
+
}
|
|
12907
|
+
function buildSqliteIntegritySql(spec) {
|
|
12908
|
+
const table = SQLITE_TABLE[spec.entity];
|
|
12909
|
+
const target = SQLITE_TABLE[spec.target];
|
|
12910
|
+
const column = `t."${spec.field}"`;
|
|
12911
|
+
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})`;
|
|
12912
|
+
const openExpr = spec.entity === "task" ? `SUM(CASE WHEN t."status" IN (${sqliteOpenStatusList()}) THEN 1 ELSE 0 END)` : "NULL";
|
|
12913
|
+
return `SELECT COUNT(*) AS count, ${openExpr} AS open_count FROM "${table}" t WHERE ${predicate}`;
|
|
12914
|
+
}
|
|
12915
|
+
var RECORD_TYPE = {
|
|
12916
|
+
task: "tasks",
|
|
12917
|
+
task_list: "task_lists",
|
|
12918
|
+
project: "projects"
|
|
12919
|
+
};
|
|
12920
|
+
function buildPostgresIntegritySql(spec, options) {
|
|
12921
|
+
const params = [options.service, RECORD_TYPE[spec.entity]];
|
|
12922
|
+
const p = (value) => {
|
|
12923
|
+
params.push(value);
|
|
12924
|
+
return `$${params.length}`;
|
|
12925
|
+
};
|
|
12926
|
+
const column = `t.payload->>'${spec.field}'`;
|
|
12927
|
+
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})`;
|
|
12928
|
+
const openExpr = spec.entity === "task" ? `COUNT(*) FILTER (WHERE t.payload->>'status' IN (${OPEN_TASK_STATUSES.map((status) => p(status)).join(", ")}))::int` : "NULL::int";
|
|
12929
|
+
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}`;
|
|
12930
|
+
return { sql, params };
|
|
12931
|
+
}
|
|
12932
|
+
function referenceOf(row, field) {
|
|
12933
|
+
const raw = row[field];
|
|
12934
|
+
if (typeof raw !== "string")
|
|
12935
|
+
return null;
|
|
12936
|
+
const trimmed = raw.trim();
|
|
12937
|
+
return trimmed === "" ? null : trimmed;
|
|
12938
|
+
}
|
|
12939
|
+
function measureIntegrityRows(spec, sets) {
|
|
12940
|
+
const rows = spec.entity === "task" ? sets.tasks : sets.taskLists;
|
|
12941
|
+
if (!rows)
|
|
12942
|
+
return null;
|
|
12943
|
+
const registered = spec.kind === "dangling" ? spec.target === "project" ? sets.projectIds : sets.taskListIds : undefined;
|
|
12944
|
+
if (spec.kind === "dangling" && !registered)
|
|
12945
|
+
return null;
|
|
12946
|
+
let count = 0;
|
|
12947
|
+
let open = 0;
|
|
12948
|
+
for (const row of rows) {
|
|
12949
|
+
const reference = referenceOf(row, spec.field);
|
|
12950
|
+
const matches = spec.kind === "missing" ? reference === null : reference !== null && !registered.has(reference);
|
|
12951
|
+
if (!matches)
|
|
12952
|
+
continue;
|
|
12953
|
+
count++;
|
|
12954
|
+
if (spec.entity === "task") {
|
|
12955
|
+
const status = row.status;
|
|
12956
|
+
if (typeof status === "string" && OPEN_TASK_STATUSES.includes(status))
|
|
12957
|
+
open++;
|
|
12958
|
+
}
|
|
12959
|
+
}
|
|
12960
|
+
return { count, open_count: spec.entity === "task" ? open : null };
|
|
12961
|
+
}
|
|
12962
|
+
|
|
12963
|
+
// src/db/integrity.ts
|
|
12964
|
+
var REQUIRED_TABLES = { tasks: true, task_lists: true, projects: true };
|
|
12965
|
+
function tableExists(db, table) {
|
|
12966
|
+
return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
|
|
12967
|
+
}
|
|
12968
|
+
function scanSqliteIntegrity(db = getDatabase()) {
|
|
12969
|
+
const missing = Object.keys(REQUIRED_TABLES).filter((table) => !tableExists(db, table));
|
|
12970
|
+
const conditions = INTEGRITY_CONDITIONS.map((spec) => {
|
|
12971
|
+
if (missing.length > 0) {
|
|
12972
|
+
return unverifiedCondition(spec, `local schema is missing table(s): ${missing.join(", ")}`);
|
|
12973
|
+
}
|
|
12974
|
+
try {
|
|
12975
|
+
const row = db.query(buildSqliteIntegritySql(spec)).get();
|
|
12976
|
+
return measuredCondition(spec, { count: Number(row?.count ?? 0), open_count: spec.entity === "task" ? Number(row?.open_count ?? 0) : null }, "sqlite");
|
|
12977
|
+
} catch (error) {
|
|
12978
|
+
return unverifiedCondition(spec, error instanceof Error ? error.message : String(error));
|
|
12979
|
+
}
|
|
12980
|
+
});
|
|
12981
|
+
return buildIntegrityReport(conditions, now());
|
|
12982
|
+
}
|
|
12983
|
+
|
|
12643
12984
|
// src/storage/sqlite-snapshot.ts
|
|
12644
12985
|
init_database();
|
|
12645
12986
|
init_audit();
|
|
@@ -13031,15 +13372,15 @@ function resolveTaskRefLocal(db, ref) {
|
|
|
13031
13372
|
return null;
|
|
13032
13373
|
if (TASK_UUID_RE.test(raw))
|
|
13033
13374
|
return getTask(raw, db);
|
|
13034
|
-
const prefixRows = db.query("SELECT id FROM tasks WHERE LOWER(id) LIKE ? ESCAPE '\\' LIMIT 2").all(`${raw.replace(/[\\%_]/g, (c) => `\\${c}`)}%`);
|
|
13375
|
+
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}`)}%`);
|
|
13035
13376
|
if (prefixRows.length > 1) {
|
|
13036
|
-
throw new
|
|
13377
|
+
throw new TaskReferenceAmbiguousError(ref, prefixRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
|
|
13037
13378
|
}
|
|
13038
13379
|
if (prefixRows.length === 1)
|
|
13039
13380
|
return getTask(prefixRows[0].id, db);
|
|
13040
|
-
const shortIdRows = db.query("SELECT id FROM tasks WHERE LOWER(short_id) = ? LIMIT 2").all(raw);
|
|
13381
|
+
const shortIdRows = db.query("SELECT id, project_id FROM tasks WHERE LOWER(short_id) = ? ORDER BY project_id, id LIMIT 2").all(raw);
|
|
13041
13382
|
if (shortIdRows.length > 1) {
|
|
13042
|
-
throw new
|
|
13383
|
+
throw new TaskReferenceAmbiguousError(ref, shortIdRows.map((row) => ({ task_id: row.id, project_id: row.project_id })));
|
|
13043
13384
|
}
|
|
13044
13385
|
if (shortIdRows.length === 1)
|
|
13045
13386
|
return getTask(shortIdRows[0].id, db);
|
|
@@ -13188,6 +13529,9 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
13188
13529
|
exportSnapshot: () => exportSqliteTodosStorageSnapshot(database()),
|
|
13189
13530
|
importSnapshot: (snapshot) => importSqliteTodosStorageSnapshot(snapshot, database())
|
|
13190
13531
|
},
|
|
13532
|
+
integrity: {
|
|
13533
|
+
report: () => scanSqliteIntegrity(database())
|
|
13534
|
+
},
|
|
13191
13535
|
transaction: (fn) => {
|
|
13192
13536
|
const tx = database().transaction(() => fn(adapter));
|
|
13193
13537
|
return tx();
|
|
@@ -13196,6 +13540,10 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
13196
13540
|
return adapter;
|
|
13197
13541
|
}
|
|
13198
13542
|
|
|
13543
|
+
// src/storage/postgres-adapter.ts
|
|
13544
|
+
init_types();
|
|
13545
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
13546
|
+
|
|
13199
13547
|
// src/storage/postgres-sync.ts
|
|
13200
13548
|
var DEFAULT_TODOS_POSTGRES_SYNC_TABLE = "todos_sync_records";
|
|
13201
13549
|
var DEFAULT_TODOS_POSTGRES_CURSOR_TABLE = "todos_sync_cursors";
|
|
@@ -13585,54 +13933,7 @@ function assertSafeIdentifier(value) {
|
|
|
13585
13933
|
throw new Error(`Unsafe Postgres identifier: ${value}`);
|
|
13586
13934
|
}
|
|
13587
13935
|
|
|
13588
|
-
// src/storage/hybrid.ts
|
|
13589
|
-
function createHybridTodosStorageAdapter(options) {
|
|
13590
|
-
const local = options.localAdapter ?? createLocalSqliteTodosStorageAdapter(options.local);
|
|
13591
|
-
const syncStore = options.syncStore ?? (options.postgresClient ? createPostgresTodosSyncStore(options.postgresClient, { sourceMachineId: options.sourceMachineId }) : null);
|
|
13592
|
-
if (!syncStore)
|
|
13593
|
-
throw new Error("hybrid storage requires a Postgres sync store or query client");
|
|
13594
|
-
if (!local.sync.exportSnapshot || !local.sync.importSnapshot) {
|
|
13595
|
-
throw new Error("hybrid storage requires local snapshot export/import support");
|
|
13596
|
-
}
|
|
13597
|
-
const exportSnapshot = async (context) => await local.sync.exportSnapshot(context);
|
|
13598
|
-
const importSnapshot = async (snapshot, context) => await local.sync.importSnapshot(snapshot, context);
|
|
13599
|
-
const remote = {
|
|
13600
|
-
ensureSchema: () => syncStore.ensureSchema(),
|
|
13601
|
-
async pushSnapshot(context) {
|
|
13602
|
-
const snapshot = await exportSnapshot(context);
|
|
13603
|
-
return syncStore.pushSnapshot(snapshot, context);
|
|
13604
|
-
},
|
|
13605
|
-
async pullSnapshot(options2, context) {
|
|
13606
|
-
const snapshot = await syncStore.pullSnapshot(options2);
|
|
13607
|
-
return importSnapshot(snapshot, context);
|
|
13608
|
-
},
|
|
13609
|
-
async syncOnce(options2, context) {
|
|
13610
|
-
const pulled = await remote.pullSnapshot(options2, context);
|
|
13611
|
-
const pushed = await remote.pushSnapshot(context);
|
|
13612
|
-
return { pulled, pushed };
|
|
13613
|
-
}
|
|
13614
|
-
};
|
|
13615
|
-
return {
|
|
13616
|
-
...local,
|
|
13617
|
-
kind: "hybrid",
|
|
13618
|
-
capabilities: {
|
|
13619
|
-
...local.capabilities,
|
|
13620
|
-
localPersistence: true,
|
|
13621
|
-
remotePersistence: true,
|
|
13622
|
-
sync: true
|
|
13623
|
-
},
|
|
13624
|
-
sync: {
|
|
13625
|
-
...local.sync,
|
|
13626
|
-
exportSnapshot,
|
|
13627
|
-
importSnapshot
|
|
13628
|
-
},
|
|
13629
|
-
remote
|
|
13630
|
-
};
|
|
13631
|
-
}
|
|
13632
|
-
|
|
13633
13936
|
// src/storage/postgres-adapter.ts
|
|
13634
|
-
init_types();
|
|
13635
|
-
import { randomUUID as randomUUID3 } from "crypto";
|
|
13636
13937
|
init_redaction();
|
|
13637
13938
|
function createPostgresTodosStorageAdapter(options) {
|
|
13638
13939
|
const store = new PostgresJsonRecordStore(options);
|
|
@@ -13760,6 +14061,9 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
13760
14061
|
exportSnapshot: () => exportSnapshot(store),
|
|
13761
14062
|
importSnapshot: (snapshot, context) => importSnapshot(snapshot, store, context)
|
|
13762
14063
|
},
|
|
14064
|
+
integrity: {
|
|
14065
|
+
report: () => store.integrityReport()
|
|
14066
|
+
},
|
|
13763
14067
|
transaction: (fn) => fn(adapter)
|
|
13764
14068
|
};
|
|
13765
14069
|
return adapter;
|
|
@@ -13866,8 +14170,9 @@ class PostgresJsonRecordStore {
|
|
|
13866
14170
|
conds.push(`payload->>'agent_id' = ${p(filter.agent_id)}`);
|
|
13867
14171
|
if (filter.session_id !== undefined)
|
|
13868
14172
|
conds.push(`payload->>'session_id' = ${p(filter.session_id)}`);
|
|
13869
|
-
if (filter.tags?.length)
|
|
13870
|
-
conds.push(`payload->'tags'
|
|
14173
|
+
if (filter.tags?.length) {
|
|
14174
|
+
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)})`);
|
|
14175
|
+
}
|
|
13871
14176
|
if (filter.has_recurrence !== undefined) {
|
|
13872
14177
|
conds.push(`(COALESCE(payload->>'recurrence_rule', '') <> '') = ${p(filter.has_recurrence)}`);
|
|
13873
14178
|
}
|
|
@@ -13928,7 +14233,8 @@ class PostgresJsonRecordStore {
|
|
|
13928
14233
|
AND object_id COLLATE "C" >= $2 AND object_id COLLATE "C" < $3
|
|
13929
14234
|
LIMIT 2`, [this.service, raw, upper]);
|
|
13930
14235
|
if (prefixResult.rows.length > 1) {
|
|
13931
|
-
|
|
14236
|
+
const candidates = prefixResult.rows.map((row) => payloadRecord2(row.payload));
|
|
14237
|
+
throw new TaskReferenceAmbiguousError(ref, candidates.map((task) => ({ task_id: task.id, project_id: task.project_id })));
|
|
13932
14238
|
}
|
|
13933
14239
|
if (prefixResult.rows.length === 1) {
|
|
13934
14240
|
return payloadRecord2(prefixResult.rows[0].payload);
|
|
@@ -13939,7 +14245,8 @@ class PostgresJsonRecordStore {
|
|
|
13939
14245
|
AND LOWER(payload->>'short_id') = $2
|
|
13940
14246
|
LIMIT 2`, [this.service, raw]);
|
|
13941
14247
|
if (shortIdResult.rows.length > 1) {
|
|
13942
|
-
|
|
14248
|
+
const candidates = shortIdResult.rows.map((row) => payloadRecord2(row.payload));
|
|
14249
|
+
throw new TaskReferenceAmbiguousError(ref, candidates.map((task) => ({ task_id: task.id, project_id: task.project_id })));
|
|
13943
14250
|
}
|
|
13944
14251
|
if (shortIdResult.rows.length === 1) {
|
|
13945
14252
|
return payloadRecord2(shortIdResult.rows[0].payload);
|
|
@@ -13953,6 +14260,24 @@ class PostgresJsonRecordStore {
|
|
|
13953
14260
|
const result = await this.options.client.query(sql, params);
|
|
13954
14261
|
return Number(result.rows[0]?.count ?? 0);
|
|
13955
14262
|
}
|
|
14263
|
+
async integrityReport() {
|
|
14264
|
+
await this.ensureSchema();
|
|
14265
|
+
const conditions = [];
|
|
14266
|
+
for (const spec of INTEGRITY_CONDITIONS) {
|
|
14267
|
+
const { sql, params } = buildPostgresIntegritySql(spec, { table: this.tableName, service: this.service });
|
|
14268
|
+
try {
|
|
14269
|
+
const result = await this.options.client.query(sql, params);
|
|
14270
|
+
const row = result.rows[0];
|
|
14271
|
+
conditions.push(measuredCondition(spec, {
|
|
14272
|
+
count: Number(row?.count ?? 0),
|
|
14273
|
+
open_count: spec.entity === "task" ? Number(row?.open_count ?? 0) : null
|
|
14274
|
+
}, "postgres"));
|
|
14275
|
+
} catch (error) {
|
|
14276
|
+
conditions.push(unverifiedCondition(spec, error instanceof Error ? error.message : String(error)));
|
|
14277
|
+
}
|
|
14278
|
+
}
|
|
14279
|
+
return buildIntegrityReport(conditions, new Date().toISOString());
|
|
14280
|
+
}
|
|
13956
14281
|
async listTombstones() {
|
|
13957
14282
|
await this.ensureSchema();
|
|
13958
14283
|
const result = await this.options.client.query(`SELECT object_type, object_id, payload, updated_at, deleted_at, source_machine_id, version
|
|
@@ -14428,7 +14753,7 @@ async function startTask2(id, agentId, store) {
|
|
|
14428
14753
|
if (task.status !== "pending" && task.status !== "in_progress") {
|
|
14429
14754
|
throw new Error(`Task is ${task.status} and cannot be started by ${agentId}`);
|
|
14430
14755
|
}
|
|
14431
|
-
|
|
14756
|
+
const started = await patchTask(task, {
|
|
14432
14757
|
status: "in_progress",
|
|
14433
14758
|
assigned_to: task.assigned_to ?? agentId,
|
|
14434
14759
|
agent_id: task.agent_id ?? agentId,
|
|
@@ -14436,6 +14761,8 @@ async function startTask2(id, agentId, store) {
|
|
|
14436
14761
|
locked_at: new Date().toISOString(),
|
|
14437
14762
|
started_at: task.started_at ?? new Date().toISOString()
|
|
14438
14763
|
}, store);
|
|
14764
|
+
await logTaskChange2(task.id, "start", "status", task.status, "in_progress", agentId, store);
|
|
14765
|
+
return started;
|
|
14439
14766
|
}
|
|
14440
14767
|
async function completeTask2(id, agentId, options, store) {
|
|
14441
14768
|
const task = await store.completeTask(id, agentId, options);
|
|
@@ -14553,9 +14880,11 @@ async function removeDependency2(taskId, dependsOn, store) {
|
|
|
14553
14880
|
}
|
|
14554
14881
|
async function listDependencies(taskId, store) {
|
|
14555
14882
|
const edges = await store.list("dependencies");
|
|
14883
|
+
const incoming = edges.filter((edge) => edge.depends_on === taskId).map((edge) => ({ task_id: edge.task_id, depends_on: edge.depends_on }));
|
|
14556
14884
|
return {
|
|
14557
14885
|
dependencies: edges.filter((edge) => edge.task_id === taskId).map((edge) => ({ task_id: edge.task_id, depends_on: edge.depends_on })),
|
|
14558
|
-
|
|
14886
|
+
blocks: incoming,
|
|
14887
|
+
blocked_by: incoming
|
|
14559
14888
|
};
|
|
14560
14889
|
}
|
|
14561
14890
|
async function addVerification(input, store, context) {
|
|
@@ -15542,17 +15871,17 @@ function createTodosCloudQueryClientFromEnv(env = process.env, options = {}) {
|
|
|
15542
15871
|
init_config();
|
|
15543
15872
|
function createTodosStorageAdapter(options = {}) {
|
|
15544
15873
|
const config = options.config ?? loadTodosStorageConfig(options.env);
|
|
15545
|
-
if (config.mode === "
|
|
15874
|
+
if (config.mode === "sqlite") {
|
|
15546
15875
|
if (isTodosShadowEnabled(options.env))
|
|
15547
15876
|
return createShadowAdapter(options, config);
|
|
15548
15877
|
return createLocalSqliteTodosStorageAdapter(options.local);
|
|
15549
15878
|
}
|
|
15550
15879
|
assertTodosRemoteStorageConfig(config);
|
|
15551
|
-
const adapter =
|
|
15880
|
+
const adapter = options.remoteAdapter ?? createImplicitPostgresAdapter(options);
|
|
15552
15881
|
if (!adapter) {
|
|
15553
|
-
throw new Error(
|
|
15882
|
+
throw new Error("postgres storage requires a repo-native Postgres adapter. " + "Pass remoteAdapter or postgresClient after wiring Postgres RDS support.");
|
|
15554
15883
|
}
|
|
15555
|
-
|
|
15884
|
+
assertPostgresAdapterCapabilities(adapter);
|
|
15556
15885
|
return adapter;
|
|
15557
15886
|
}
|
|
15558
15887
|
function createShadowAdapter(options, config) {
|
|
@@ -15566,7 +15895,7 @@ function createShadowAdapter(options, config) {
|
|
|
15566
15895
|
...options.local ? { local: options.local } : {},
|
|
15567
15896
|
...options.postgresSyncStore ? { syncStore: options.postgresSyncStore } : {},
|
|
15568
15897
|
...postgresClient ? { postgresClient } : {},
|
|
15569
|
-
...options.
|
|
15898
|
+
...options.sourceMachineId ? { sourceMachineId: options.sourceMachineId } : {}
|
|
15570
15899
|
});
|
|
15571
15900
|
}
|
|
15572
15901
|
function createImplicitPostgresAdapter(options) {
|
|
@@ -15574,25 +15903,12 @@ function createImplicitPostgresAdapter(options) {
|
|
|
15574
15903
|
return null;
|
|
15575
15904
|
return createPostgresTodosStorageAdapter({
|
|
15576
15905
|
client: options.postgresClient,
|
|
15577
|
-
...options.
|
|
15578
|
-
});
|
|
15579
|
-
}
|
|
15580
|
-
function createImplicitHybridAdapter(options) {
|
|
15581
|
-
if (!options.postgresClient && !options.postgresSyncStore)
|
|
15582
|
-
return null;
|
|
15583
|
-
return createHybridTodosStorageAdapter({
|
|
15584
|
-
...options.hybrid ?? {},
|
|
15585
|
-
local: options.local,
|
|
15586
|
-
postgresClient: options.postgresClient,
|
|
15587
|
-
syncStore: options.postgresSyncStore
|
|
15906
|
+
...options.sourceMachineId ? { sourceMachineId: options.sourceMachineId } : {}
|
|
15588
15907
|
});
|
|
15589
15908
|
}
|
|
15590
|
-
function
|
|
15909
|
+
function assertPostgresAdapterCapabilities(adapter) {
|
|
15591
15910
|
if (!adapter.capabilities.remotePersistence) {
|
|
15592
|
-
throw new Error(
|
|
15593
|
-
}
|
|
15594
|
-
if (mode === "hybrid" && !adapter.capabilities.localPersistence) {
|
|
15595
|
-
throw new Error("hybrid storage adapter must also set capabilities.localPersistence=true");
|
|
15911
|
+
throw new Error("postgres storage adapter must set capabilities.remotePersistence=true");
|
|
15596
15912
|
}
|
|
15597
15913
|
}
|
|
15598
15914
|
// src/storage/shadow-outbox.ts
|
|
@@ -15919,6 +16235,50 @@ async function closeRuntimeShadowCloud() {
|
|
|
15919
16235
|
await cloud.close().catch(() => {});
|
|
15920
16236
|
}
|
|
15921
16237
|
}
|
|
16238
|
+
// src/storage/hybrid.ts
|
|
16239
|
+
function createHybridTodosStorageAdapter(options) {
|
|
16240
|
+
const local = options.localAdapter ?? createLocalSqliteTodosStorageAdapter(options.local);
|
|
16241
|
+
const syncStore = options.syncStore ?? (options.postgresClient ? createPostgresTodosSyncStore(options.postgresClient, { sourceMachineId: options.sourceMachineId }) : null);
|
|
16242
|
+
if (!syncStore)
|
|
16243
|
+
throw new Error("hybrid storage requires a Postgres sync store or query client");
|
|
16244
|
+
if (!local.sync.exportSnapshot || !local.sync.importSnapshot) {
|
|
16245
|
+
throw new Error("hybrid storage requires local snapshot export/import support");
|
|
16246
|
+
}
|
|
16247
|
+
const exportSnapshot2 = async (context) => await local.sync.exportSnapshot(context);
|
|
16248
|
+
const importSnapshot2 = async (snapshot, context) => await local.sync.importSnapshot(snapshot, context);
|
|
16249
|
+
const remote = {
|
|
16250
|
+
ensureSchema: () => syncStore.ensureSchema(),
|
|
16251
|
+
async pushSnapshot(context) {
|
|
16252
|
+
const snapshot = await exportSnapshot2(context);
|
|
16253
|
+
return syncStore.pushSnapshot(snapshot, context);
|
|
16254
|
+
},
|
|
16255
|
+
async pullSnapshot(options2, context) {
|
|
16256
|
+
const snapshot = await syncStore.pullSnapshot(options2);
|
|
16257
|
+
return importSnapshot2(snapshot, context);
|
|
16258
|
+
},
|
|
16259
|
+
async syncOnce(options2, context) {
|
|
16260
|
+
const pulled = await remote.pullSnapshot(options2, context);
|
|
16261
|
+
const pushed = await remote.pushSnapshot(context);
|
|
16262
|
+
return { pulled, pushed };
|
|
16263
|
+
}
|
|
16264
|
+
};
|
|
16265
|
+
return {
|
|
16266
|
+
...local,
|
|
16267
|
+
kind: "hybrid",
|
|
16268
|
+
capabilities: {
|
|
16269
|
+
...local.capabilities,
|
|
16270
|
+
localPersistence: true,
|
|
16271
|
+
remotePersistence: true,
|
|
16272
|
+
sync: true
|
|
16273
|
+
},
|
|
16274
|
+
sync: {
|
|
16275
|
+
...local.sync,
|
|
16276
|
+
exportSnapshot: exportSnapshot2,
|
|
16277
|
+
importSnapshot: importSnapshot2
|
|
16278
|
+
},
|
|
16279
|
+
remote
|
|
16280
|
+
};
|
|
16281
|
+
}
|
|
15922
16282
|
// src/storage/comment-redaction-backfill.ts
|
|
15923
16283
|
init_redaction();
|
|
15924
16284
|
var COMMENT_REDACTION_BACKFILL_CONFIRMATION = "REDACT_STORED_TODOS_COMMENTS";
|
|
@@ -16453,11 +16813,13 @@ export {
|
|
|
16453
16813
|
postgresTodosCommentCursorIndexSql,
|
|
16454
16814
|
planRunArtifactsS3Sync,
|
|
16455
16815
|
parseStorageMode,
|
|
16816
|
+
parseStorageBackend,
|
|
16456
16817
|
maybeInstallShadowCapture2 as maybeInstallShadowCapture,
|
|
16457
16818
|
loadTodosStorageConfig,
|
|
16458
16819
|
loadStorageConfig,
|
|
16459
16820
|
isTodosShadowEnabled,
|
|
16460
16821
|
isTodosRemoteStorageEnabled,
|
|
16822
|
+
isTodosPostgresBackend,
|
|
16461
16823
|
isCommentRedactionBackfillComplete,
|
|
16462
16824
|
installShadowOutboxSchema,
|
|
16463
16825
|
importSqliteTodosStorageSnapshot,
|
|
@@ -16466,6 +16828,7 @@ export {
|
|
|
16466
16828
|
getTodosStorageEnvName,
|
|
16467
16829
|
getTodosStorageDatabaseUrl,
|
|
16468
16830
|
getTodosStorageDatabaseEnv,
|
|
16831
|
+
getTodosStorageBackend,
|
|
16469
16832
|
getStorageMode,
|
|
16470
16833
|
getStorageDatabaseUrl,
|
|
16471
16834
|
getStorageDatabaseEnv,
|