@hasna/todos 0.15.19 → 0.15.24
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/dist/ai-tools.d.ts +80 -0
- package/dist/ai-tools.d.ts.map +1 -0
- package/dist/ai.d.ts +313 -0
- package/dist/ai.d.ts.map +1 -0
- package/dist/cli/cloud-router.d.ts +30 -2
- package/dist/cli/cloud-router.d.ts.map +1 -1
- package/dist/cli/commands/ai-commands.d.ts +3 -0
- package/dist/cli/commands/ai-commands.d.ts.map +1 -0
- package/dist/cli/commands/help-commands.d.ts +2 -1
- package/dist/cli/commands/help-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/index.js +3685 -572
- package/dist/cli/stage-a.d.ts +34 -12
- package/dist/cli/stage-a.d.ts.map +1 -1
- package/dist/contracts.d.ts +1 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +861 -29
- package/dist/db/audit.d.ts +8 -0
- package/dist/db/audit.d.ts.map +1 -1
- package/dist/db/task-lifecycle.d.ts +7 -1
- package/dist/db/task-lifecycle.d.ts.map +1 -1
- package/dist/db/tasks.d.ts +1 -1
- package/dist/db/tasks.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3717 -1205
- package/dist/lib/cli-help.d.ts +3 -2
- package/dist/lib/cli-help.d.ts.map +1 -1
- package/dist/lib/config.d.ts +4 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/saved-search-views.d.ts.map +1 -1
- package/dist/lib/stale-lock-handoff.d.ts +25 -0
- package/dist/lib/stale-lock-handoff.d.ts.map +1 -0
- package/dist/mcp/index.js +822 -101
- package/dist/mcp.js +3 -1
- package/dist/project-registration.js +4208 -3709
- package/dist/registry.js +826 -29
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/index.js +8 -1
- package/dist/sdk/v1.generated.d.ts +29 -0
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/index.js +1155 -434
- package/dist/server/openapi.d.ts +189 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage/audit-history-import.d.ts +14 -0
- package/dist/storage/audit-history-import.d.ts.map +1 -0
- package/dist/storage/interfaces.d.ts +12 -1
- 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/sqlite-snapshot.d.ts.map +1 -1
- package/dist/storage.js +531 -34
- package/dist/task-manifest.js +30 -4
- package/dist/types/index.d.ts +43 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +3 -1
package/dist/mcp/index.js
CHANGED
|
@@ -47,7 +47,7 @@ function isBlockingDependencyStatus(status) {
|
|
|
47
47
|
function isTerminalStatus(status) {
|
|
48
48
|
return status === "completed" || status === "failed" || status === "cancelled";
|
|
49
49
|
}
|
|
50
|
-
var TASK_STATUSES, TASK_PRIORITIES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanRevisionConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
|
|
50
|
+
var TASK_STATUSES, TASK_PRIORITIES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanRevisionConflictError, PlanNotFoundError, LockError, StaleLockHandoffError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
|
|
51
51
|
var init_types = __esm(() => {
|
|
52
52
|
TASK_STATUSES = [
|
|
53
53
|
"pending",
|
|
@@ -169,6 +169,16 @@ var init_types = __esm(() => {
|
|
|
169
169
|
this.name = "LockError";
|
|
170
170
|
}
|
|
171
171
|
};
|
|
172
|
+
StaleLockHandoffError = class StaleLockHandoffError extends Error {
|
|
173
|
+
code;
|
|
174
|
+
details;
|
|
175
|
+
constructor(code, message, details = {}) {
|
|
176
|
+
super(message);
|
|
177
|
+
this.code = code;
|
|
178
|
+
this.details = details;
|
|
179
|
+
this.name = "StaleLockHandoffError";
|
|
180
|
+
}
|
|
181
|
+
};
|
|
172
182
|
AgentNotFoundError = class AgentNotFoundError extends Error {
|
|
173
183
|
agentId;
|
|
174
184
|
static code = "AGENT_NOT_FOUND";
|
|
@@ -9413,7 +9423,7 @@ var init_zod = __esm(() => {
|
|
|
9413
9423
|
});
|
|
9414
9424
|
|
|
9415
9425
|
// src/lib/config.ts
|
|
9416
|
-
import { existsSync as existsSync4 } from "fs";
|
|
9426
|
+
import { existsSync as existsSync4, readFileSync as readFileSync2 } from "fs";
|
|
9417
9427
|
import { dirname as dirname2, join as join3 } from "path";
|
|
9418
9428
|
function getConfigPath() {
|
|
9419
9429
|
return join3(getTodosGlobalDir(), "config.json");
|
|
@@ -12170,28 +12180,55 @@ var init_activity_audit = __esm(() => {
|
|
|
12170
12180
|
function sanitizeHistoryValue(value, context) {
|
|
12171
12181
|
return value === undefined || value === null ? null : sanitizePreWriteText(String(value), context);
|
|
12172
12182
|
}
|
|
12173
|
-
function
|
|
12183
|
+
function insertTaskHistory(entry, db) {
|
|
12174
12184
|
const d = db || getDatabase();
|
|
12175
|
-
const
|
|
12176
|
-
|
|
12177
|
-
|
|
12178
|
-
|
|
12179
|
-
|
|
12185
|
+
const safeEntry = {
|
|
12186
|
+
...entry,
|
|
12187
|
+
field: entry.field || null,
|
|
12188
|
+
old_value: sanitizeHistoryValue(entry.old_value, "task_history.old_value"),
|
|
12189
|
+
new_value: sanitizeHistoryValue(entry.new_value, "task_history.new_value"),
|
|
12190
|
+
agent_id: entry.agent_id || null,
|
|
12191
|
+
machine_id: entry.machine_id ?? currentStorageMachineId(d)
|
|
12192
|
+
};
|
|
12180
12193
|
d.run(`INSERT INTO task_history (id, task_id, action, field, old_value, new_value, agent_id, created_at, machine_id)
|
|
12181
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
12194
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
12195
|
+
safeEntry.id,
|
|
12196
|
+
safeEntry.task_id,
|
|
12197
|
+
safeEntry.action,
|
|
12198
|
+
safeEntry.field,
|
|
12199
|
+
safeEntry.old_value,
|
|
12200
|
+
safeEntry.new_value,
|
|
12201
|
+
safeEntry.agent_id,
|
|
12202
|
+
safeEntry.created_at,
|
|
12203
|
+
safeEntry.machine_id ?? null
|
|
12204
|
+
]);
|
|
12182
12205
|
try {
|
|
12183
12206
|
const { logActivity: logActivity2 } = (init_activity_audit(), __toCommonJS(exports_activity_audit));
|
|
12184
12207
|
logActivity2({
|
|
12185
12208
|
entity_type: "task",
|
|
12186
|
-
entity_id:
|
|
12187
|
-
action,
|
|
12188
|
-
field,
|
|
12189
|
-
old_value:
|
|
12190
|
-
new_value:
|
|
12191
|
-
actor_id:
|
|
12209
|
+
entity_id: safeEntry.task_id,
|
|
12210
|
+
action: safeEntry.action,
|
|
12211
|
+
field: safeEntry.field ?? undefined,
|
|
12212
|
+
old_value: safeEntry.old_value,
|
|
12213
|
+
new_value: safeEntry.new_value,
|
|
12214
|
+
actor_id: safeEntry.agent_id ?? undefined
|
|
12192
12215
|
}, d);
|
|
12193
12216
|
} catch {}
|
|
12194
|
-
return
|
|
12217
|
+
return safeEntry;
|
|
12218
|
+
}
|
|
12219
|
+
function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
|
|
12220
|
+
const d = db || getDatabase();
|
|
12221
|
+
return insertTaskHistory({
|
|
12222
|
+
id: uuid(),
|
|
12223
|
+
task_id: taskId,
|
|
12224
|
+
action,
|
|
12225
|
+
field: field || null,
|
|
12226
|
+
old_value: oldValue ?? null,
|
|
12227
|
+
new_value: newValue ?? null,
|
|
12228
|
+
agent_id: agentId || null,
|
|
12229
|
+
created_at: now(),
|
|
12230
|
+
machine_id: currentStorageMachineId(d)
|
|
12231
|
+
}, d);
|
|
12195
12232
|
}
|
|
12196
12233
|
function getTaskHistory(taskId, db) {
|
|
12197
12234
|
const d = db || getDatabase();
|
|
@@ -13194,6 +13231,142 @@ var init_task_graph = __esm(() => {
|
|
|
13194
13231
|
init_task_crud();
|
|
13195
13232
|
});
|
|
13196
13233
|
|
|
13234
|
+
// src/lib/stale-lock-handoff.ts
|
|
13235
|
+
function normalizeExactTaskId(value) {
|
|
13236
|
+
if (typeof value !== "string" || !EXACT_TASK_UUID_RE.test(value.trim())) {
|
|
13237
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_TASK_ID", "stale-lock handoff requires one exact full task UUID", { task_id: typeof value === "string" ? value : null });
|
|
13238
|
+
}
|
|
13239
|
+
return value.trim().toLowerCase();
|
|
13240
|
+
}
|
|
13241
|
+
function requireNonEmptyString(value, field) {
|
|
13242
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
13243
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", `${field} must be a non-empty string`, { field });
|
|
13244
|
+
}
|
|
13245
|
+
const trimmed = value.trim();
|
|
13246
|
+
if (field === "reason" && trimmed.length > MAX_REASON_LENGTH) {
|
|
13247
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", `reason must be at most ${MAX_REASON_LENGTH} characters`, { field, max_length: MAX_REASON_LENGTH });
|
|
13248
|
+
}
|
|
13249
|
+
return trimmed;
|
|
13250
|
+
}
|
|
13251
|
+
function requireCanonicalLockVersion(value) {
|
|
13252
|
+
if (typeof value !== "string" || !CANONICAL_LOCK_VERSION_RE.test(value)) {
|
|
13253
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "expected_lock_version must be the exact canonical locked_at timestamp (YYYY-MM-DDTHH:mm:ss.sssZ)", { field: "expected_lock_version" });
|
|
13254
|
+
}
|
|
13255
|
+
const parsed = Date.parse(value);
|
|
13256
|
+
if (value.startsWith("0000-") || Number.isNaN(parsed) || new Date(parsed).toISOString() !== value) {
|
|
13257
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "expected_lock_version must name a real canonical UTC instant", { field: "expected_lock_version" });
|
|
13258
|
+
}
|
|
13259
|
+
return value;
|
|
13260
|
+
}
|
|
13261
|
+
function requireStaleThreshold(value) {
|
|
13262
|
+
if (!Number.isSafeInteger(value) || Number(value) <= 0) {
|
|
13263
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "stale_after_seconds must be a positive safe integer", { field: "stale_after_seconds" });
|
|
13264
|
+
}
|
|
13265
|
+
return Number(value);
|
|
13266
|
+
}
|
|
13267
|
+
function prepareStaleLockHandoff(input, options = {}) {
|
|
13268
|
+
const taskId = normalizeExactTaskId(input.task_id);
|
|
13269
|
+
const actor = requireNonEmptyString(input.actor, "actor");
|
|
13270
|
+
const expectedHolder = requireNonEmptyString(input.expected_holder, "expected_holder");
|
|
13271
|
+
const newHolder = requireNonEmptyString(input.new_holder, "new_holder");
|
|
13272
|
+
const expectedLockVersion = requireCanonicalLockVersion(input.expected_lock_version);
|
|
13273
|
+
const staleAfterSeconds = requireStaleThreshold(input.stale_after_seconds);
|
|
13274
|
+
const reason = sanitizePreWriteText(requireNonEmptyString(input.reason, "reason"), "stale_lock_handoff.reason").trim();
|
|
13275
|
+
if (!reason) {
|
|
13276
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "reason must remain non-empty after safety filtering", { field: "reason" });
|
|
13277
|
+
}
|
|
13278
|
+
if (canonicalAgentRef(actor) !== canonicalAgentRef(newHolder)) {
|
|
13279
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_ACTOR_MISMATCH", "new_holder must match the authenticated actor", { actor, new_holder: newHolder });
|
|
13280
|
+
}
|
|
13281
|
+
const operationTimestamp = options.now ?? new Date().toISOString();
|
|
13282
|
+
if (!CANONICAL_LOCK_VERSION_RE.test(operationTimestamp) || new Date(Date.parse(operationTimestamp)).toISOString() !== operationTimestamp) {
|
|
13283
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "operation timestamp must be a canonical UTC instant");
|
|
13284
|
+
}
|
|
13285
|
+
const staleCutoff = new Date(Date.parse(operationTimestamp) - staleAfterSeconds * 1000).toISOString();
|
|
13286
|
+
return {
|
|
13287
|
+
task_id: taskId,
|
|
13288
|
+
actor,
|
|
13289
|
+
expected_holder: expectedHolder,
|
|
13290
|
+
expected_lock_version: expectedLockVersion,
|
|
13291
|
+
stale_after_seconds: staleAfterSeconds,
|
|
13292
|
+
new_holder: newHolder,
|
|
13293
|
+
reason,
|
|
13294
|
+
operation_timestamp: operationTimestamp,
|
|
13295
|
+
stale_cutoff: staleCutoff,
|
|
13296
|
+
receipt_id: options.receiptId ?? crypto.randomUUID()
|
|
13297
|
+
};
|
|
13298
|
+
}
|
|
13299
|
+
function buildStaleLockHandoffReceipt(input) {
|
|
13300
|
+
return {
|
|
13301
|
+
schema_version: STALE_LOCK_HANDOFF_SCHEMA_VERSION,
|
|
13302
|
+
receipt_id: input.receipt_id,
|
|
13303
|
+
task_id: input.task_id,
|
|
13304
|
+
actor: input.actor,
|
|
13305
|
+
previous_holder: input.expected_holder,
|
|
13306
|
+
previous_lock_version: input.expected_lock_version,
|
|
13307
|
+
new_holder: input.new_holder,
|
|
13308
|
+
new_lock_version: input.operation_timestamp,
|
|
13309
|
+
stale_after_seconds: input.stale_after_seconds,
|
|
13310
|
+
stale_cutoff: input.stale_cutoff,
|
|
13311
|
+
reason: input.reason,
|
|
13312
|
+
created_at: input.operation_timestamp
|
|
13313
|
+
};
|
|
13314
|
+
}
|
|
13315
|
+
function staleLockHandoffHistory(receipt, machineId) {
|
|
13316
|
+
return {
|
|
13317
|
+
id: receipt.receipt_id,
|
|
13318
|
+
task_id: receipt.task_id,
|
|
13319
|
+
action: STALE_LOCK_HANDOFF_ACTION,
|
|
13320
|
+
field: STALE_LOCK_HANDOFF_FIELD,
|
|
13321
|
+
old_value: JSON.stringify({
|
|
13322
|
+
holder: receipt.previous_holder,
|
|
13323
|
+
lock_version: receipt.previous_lock_version
|
|
13324
|
+
}),
|
|
13325
|
+
new_value: JSON.stringify(receipt),
|
|
13326
|
+
agent_id: receipt.actor,
|
|
13327
|
+
created_at: receipt.created_at,
|
|
13328
|
+
machine_id: machineId
|
|
13329
|
+
};
|
|
13330
|
+
}
|
|
13331
|
+
function throwStaleLockHandoffConflict(task, input) {
|
|
13332
|
+
if (!task.locked_by || !task.locked_at) {
|
|
13333
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_NOT_LOCKED", `Task ${input.task_id} does not have a complete lock to hand off`, { task_id: input.task_id });
|
|
13334
|
+
}
|
|
13335
|
+
if (task.locked_at !== input.expected_lock_version) {
|
|
13336
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_VERSION_MISMATCH", `Task ${input.task_id} lock version changed`, {
|
|
13337
|
+
task_id: input.task_id,
|
|
13338
|
+
expected_lock_version: input.expected_lock_version,
|
|
13339
|
+
current_lock_version: task.locked_at
|
|
13340
|
+
});
|
|
13341
|
+
}
|
|
13342
|
+
if (task.locked_by !== input.expected_holder) {
|
|
13343
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_HOLDER_MISMATCH", `Task ${input.task_id} lock holder changed`, {
|
|
13344
|
+
task_id: input.task_id,
|
|
13345
|
+
expected_holder: input.expected_holder,
|
|
13346
|
+
current_holder: task.locked_by
|
|
13347
|
+
});
|
|
13348
|
+
}
|
|
13349
|
+
if (isTerminalStatus(task.status)) {
|
|
13350
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_TERMINAL", `Task ${input.task_id} is ${task.status} and cannot transfer a lock`, { task_id: input.task_id, status: task.status });
|
|
13351
|
+
}
|
|
13352
|
+
if (task.locked_at >= input.stale_cutoff) {
|
|
13353
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_NOT_STALE", `Task ${input.task_id} lock is not older than the supplied stale threshold`, {
|
|
13354
|
+
task_id: input.task_id,
|
|
13355
|
+
current_lock_version: task.locked_at,
|
|
13356
|
+
stale_cutoff: input.stale_cutoff
|
|
13357
|
+
});
|
|
13358
|
+
}
|
|
13359
|
+
throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_CONFLICT", `Task ${input.task_id} changed during stale-lock handoff`, { task_id: input.task_id });
|
|
13360
|
+
}
|
|
13361
|
+
var STALE_LOCK_HANDOFF_SCHEMA_VERSION = "todos.stale-lock-handoff.v1", STALE_LOCK_HANDOFF_ACTION = "stale_lock_handoff", STALE_LOCK_HANDOFF_FIELD = "lock", EXACT_TASK_UUID_RE, CANONICAL_LOCK_VERSION_RE, MAX_REASON_LENGTH = 4096;
|
|
13362
|
+
var init_stale_lock_handoff = __esm(() => {
|
|
13363
|
+
init_types();
|
|
13364
|
+
init_creator_identity();
|
|
13365
|
+
init_prewrite_secrets();
|
|
13366
|
+
EXACT_TASK_UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
13367
|
+
CANONICAL_LOCK_VERSION_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
13368
|
+
});
|
|
13369
|
+
|
|
13197
13370
|
// src/db/task-lifecycle.ts
|
|
13198
13371
|
var exports_task_lifecycle = {};
|
|
13199
13372
|
__export(exports_task_lifecycle, {
|
|
@@ -13202,6 +13375,7 @@ __export(exports_task_lifecycle, {
|
|
|
13202
13375
|
startTask: () => startTask,
|
|
13203
13376
|
spawnNextRecurrence: () => spawnNextRecurrence,
|
|
13204
13377
|
lockTask: () => lockTask,
|
|
13378
|
+
handoffStaleTaskLock: () => handoffStaleTaskLock,
|
|
13205
13379
|
getTasksChangedSince: () => getTasksChangedSince,
|
|
13206
13380
|
getTaskLockStatus: () => getTaskLockStatus,
|
|
13207
13381
|
getStaleTasks: () => getStaleTasks,
|
|
@@ -13470,6 +13644,38 @@ function unlockTask(id, agentId, db) {
|
|
|
13470
13644
|
WHERE id = ?`, [timestamp2, id]);
|
|
13471
13645
|
return true;
|
|
13472
13646
|
}
|
|
13647
|
+
function handoffStaleTaskLock(input, db) {
|
|
13648
|
+
const d = db || getDatabase();
|
|
13649
|
+
const prepared = prepareStaleLockHandoff(input);
|
|
13650
|
+
const receipt = buildStaleLockHandoffReceipt(prepared);
|
|
13651
|
+
const history = staleLockHandoffHistory(receipt, null);
|
|
13652
|
+
const transfer = d.transaction(() => {
|
|
13653
|
+
const result = d.run(`UPDATE tasks
|
|
13654
|
+
SET locked_by = ?, locked_at = ?, updated_at = ?, version = version + 1
|
|
13655
|
+
WHERE id = ?
|
|
13656
|
+
AND locked_by = ?
|
|
13657
|
+
AND locked_at = ?
|
|
13658
|
+
AND julianday(locked_at) < julianday(?)
|
|
13659
|
+
AND status NOT IN ('completed', 'failed', 'cancelled')`, [
|
|
13660
|
+
prepared.new_holder,
|
|
13661
|
+
prepared.operation_timestamp,
|
|
13662
|
+
prepared.operation_timestamp,
|
|
13663
|
+
prepared.task_id,
|
|
13664
|
+
prepared.expected_holder,
|
|
13665
|
+
prepared.expected_lock_version,
|
|
13666
|
+
prepared.stale_cutoff
|
|
13667
|
+
]);
|
|
13668
|
+
if (result.changes === 0) {
|
|
13669
|
+
const current = getTask(prepared.task_id, d);
|
|
13670
|
+
if (!current)
|
|
13671
|
+
throw new TaskNotFoundError(prepared.task_id);
|
|
13672
|
+
throwStaleLockHandoffConflict(current, prepared);
|
|
13673
|
+
}
|
|
13674
|
+
insertTaskHistory(history, d);
|
|
13675
|
+
});
|
|
13676
|
+
transfer();
|
|
13677
|
+
return receipt;
|
|
13678
|
+
}
|
|
13473
13679
|
function getTaskLockStatus(id, db) {
|
|
13474
13680
|
const d = db || getDatabase();
|
|
13475
13681
|
const task = getTask(id, d);
|
|
@@ -13770,6 +13976,7 @@ var init_task_lifecycle = __esm(() => {
|
|
|
13770
13976
|
init_task_crud();
|
|
13771
13977
|
init_task_graph();
|
|
13772
13978
|
init_prewrite_secrets();
|
|
13979
|
+
init_stale_lock_handoff();
|
|
13773
13980
|
});
|
|
13774
13981
|
|
|
13775
13982
|
// src/db/task-crud.ts
|
|
@@ -15789,7 +15996,7 @@ var init_boards = __esm(() => {
|
|
|
15789
15996
|
|
|
15790
15997
|
// src/lib/artifact-store.ts
|
|
15791
15998
|
import { createHash as createHash2 } from "crypto";
|
|
15792
|
-
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as
|
|
15999
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync3, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
15793
16000
|
import { basename, dirname as dirname4, join as join6, resolve as resolve7 } from "path";
|
|
15794
16001
|
import { tmpdir as tmpdir2 } from "os";
|
|
15795
16002
|
function isInMemoryDb2(path) {
|
|
@@ -15856,7 +16063,7 @@ function storeArtifactContent(input) {
|
|
|
15856
16063
|
const sourceStat = statSync2(sourcePath);
|
|
15857
16064
|
if (!sourceStat.isFile())
|
|
15858
16065
|
throw new Error(`Artifact path is not a file: ${sanitizePreWriteText(input.path, "artifact.path")}`);
|
|
15859
|
-
const sourceBuffer =
|
|
16066
|
+
const sourceBuffer = readFileSync3(sourcePath);
|
|
15860
16067
|
const sourceSha = sha256(sourceBuffer);
|
|
15861
16068
|
const textLike = isTextLike(sourceBuffer, input.path);
|
|
15862
16069
|
let storedBuffer = sourceBuffer;
|
|
@@ -15943,7 +16150,7 @@ function verifyStoredArtifact(input) {
|
|
|
15943
16150
|
message: "stored artifact content is missing"
|
|
15944
16151
|
};
|
|
15945
16152
|
}
|
|
15946
|
-
const buffer =
|
|
16153
|
+
const buffer = readFileSync3(storedPath);
|
|
15947
16154
|
const actualSha = sha256(buffer);
|
|
15948
16155
|
const actualSize = buffer.length;
|
|
15949
16156
|
const ok = actualSha === store.sha256 && actualSize === store.size_bytes;
|
|
@@ -15963,7 +16170,7 @@ function exportStoredArtifactContent(input) {
|
|
|
15963
16170
|
const report = verifyStoredArtifact(input);
|
|
15964
16171
|
if (report.status !== "ok" || !report.relative_path || !report.actual_sha256 || report.actual_size_bytes === null)
|
|
15965
16172
|
return null;
|
|
15966
|
-
const content =
|
|
16173
|
+
const content = readFileSync3(artifactStorePath(report.relative_path));
|
|
15967
16174
|
return {
|
|
15968
16175
|
artifact_id: input.id,
|
|
15969
16176
|
sha256: report.actual_sha256,
|
|
@@ -17342,6 +17549,7 @@ __export(exports_tasks, {
|
|
|
17342
17549
|
insertTaskTags: () => insertTaskTags,
|
|
17343
17550
|
importTaskBoardBundle: () => importTaskBoardBundle,
|
|
17344
17551
|
importCalendarIcs: () => importCalendarIcs,
|
|
17552
|
+
handoffStaleTaskLock: () => handoffStaleTaskLock,
|
|
17345
17553
|
getTimeReport: () => getTimeReport,
|
|
17346
17554
|
getTimeLogs: () => getTimeLogs,
|
|
17347
17555
|
getTasksChangedSince: () => getTasksChangedSince,
|
|
@@ -18405,7 +18613,7 @@ var init_token_utils = __esm(() => {
|
|
|
18405
18613
|
});
|
|
18406
18614
|
|
|
18407
18615
|
// src/lib/assignee-validation.ts
|
|
18408
|
-
import { readFileSync as
|
|
18616
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
18409
18617
|
import { homedir as homedir3 } from "os";
|
|
18410
18618
|
import { join as join7 } from "path";
|
|
18411
18619
|
function defaultSeatRosterPath() {
|
|
@@ -18413,7 +18621,7 @@ function defaultSeatRosterPath() {
|
|
|
18413
18621
|
}
|
|
18414
18622
|
function loadSeatSlugs(path = defaultSeatRosterPath()) {
|
|
18415
18623
|
try {
|
|
18416
|
-
const parsed = JSON.parse(
|
|
18624
|
+
const parsed = JSON.parse(readFileSync4(path, "utf8"));
|
|
18417
18625
|
const slugs = new Set;
|
|
18418
18626
|
for (const agent of parsed.agents ?? []) {
|
|
18419
18627
|
if (typeof agent?.slug === "string" && agent.slug.trim()) {
|
|
@@ -20799,6 +21007,7 @@ var init_http_client = __esm(() => {
|
|
|
20799
21007
|
// src/cli/cloud-router.ts
|
|
20800
21008
|
import { resolveStorageClient } from "@hasna/contracts/client/storage";
|
|
20801
21009
|
import { normalizeStorageMode } from "@hasna/contracts/mode";
|
|
21010
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
20802
21011
|
import { resolve as resolvePath } from "path";
|
|
20803
21012
|
function cleanMode(value) {
|
|
20804
21013
|
const normalized = value?.trim().toLowerCase();
|
|
@@ -21199,20 +21408,28 @@ async function cloudGetTask(client, id) {
|
|
|
21199
21408
|
const raw = await client.get("tasks", id);
|
|
21200
21409
|
return raw == null ? null : unwrapTask(raw);
|
|
21201
21410
|
}
|
|
21202
|
-
async function cloudCreateTask(client, input) {
|
|
21411
|
+
async function cloudCreateTask(client, input, verification = {}) {
|
|
21203
21412
|
const expectedParentId = typeof input["parent_id"] === "string" ? input["parent_id"] : null;
|
|
21204
|
-
const
|
|
21413
|
+
const expectedCreatedBy = typeof verification.expectedCreatedBy === "string" && verification.expectedCreatedBy.trim() ? verification.expectedCreatedBy : null;
|
|
21414
|
+
if (expectedCreatedBy !== null) {
|
|
21415
|
+
await requireTaskCreatorCapability(client);
|
|
21416
|
+
}
|
|
21417
|
+
const created = unwrapTask(await requiredRemoteRoute(client, "/v1/tasks", () => client.transport.post("/tasks", input, {
|
|
21418
|
+
idempotencyKey: randomUUID3(),
|
|
21419
|
+
retry: false
|
|
21420
|
+
}), ["PARENT_TASK_NOT_FOUND"]));
|
|
21205
21421
|
if (!created || typeof created.id !== "string" || !created.id.trim()) {
|
|
21206
21422
|
throw new Error(`REMOTE_API_INCOMPATIBLE: configured Todos authority ${remoteAuthorityBase(client)} returned a task create ` + "response without a stored task id; no success row or local SQLite fallback is permitted");
|
|
21207
21423
|
}
|
|
21208
21424
|
const persisted = await cloudGetTask(client, created.id);
|
|
21209
|
-
if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== expectedParentId) {
|
|
21210
|
-
|
|
21425
|
+
if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== expectedParentId || expectedCreatedBy !== null && persisted.created_by !== expectedCreatedBy) {
|
|
21426
|
+
const creatorDetail = expectedCreatedBy === null ? "" : ` and explicit created_by=${JSON.stringify(expectedCreatedBy)} ` + `(readback ${JSON.stringify(persisted?.created_by ?? null)})`;
|
|
21427
|
+
throw new Error(`TASK_CREATE_PERSISTENCE_UNVERIFIED: configured Todos authority ${remoteAuthorityBase(client)} accepted ` + `POST /v1/tasks but authoritative GET /v1/tasks/${encodeURIComponent(created.id)} did not return the same ` + `stored task id and parent_id${creatorDetail}; no success row or local SQLite fallback is permitted`);
|
|
21211
21428
|
}
|
|
21212
21429
|
return persisted;
|
|
21213
21430
|
}
|
|
21214
21431
|
async function cloudUpdateTask(client, id, patch) {
|
|
21215
|
-
return unwrapTask(await client.
|
|
21432
|
+
return unwrapTask(await client.transport.patch(`/tasks/${encodeURIComponent(id)}`, patch));
|
|
21216
21433
|
}
|
|
21217
21434
|
async function cloudDeleteTask(client, id) {
|
|
21218
21435
|
try {
|
|
@@ -21228,6 +21445,51 @@ async function cloudTaskAction(client, id, action, body = {}) {
|
|
|
21228
21445
|
const raw = await client.transport.post(`/tasks/${encodeURIComponent(id)}/${action}`, body);
|
|
21229
21446
|
return unwrapTask(raw);
|
|
21230
21447
|
}
|
|
21448
|
+
function resolveOpenApiSchema(document, schema) {
|
|
21449
|
+
if (!schema || typeof schema !== "object" || Array.isArray(schema))
|
|
21450
|
+
return null;
|
|
21451
|
+
const value = schema;
|
|
21452
|
+
if (typeof value["$ref"] !== "string")
|
|
21453
|
+
return value;
|
|
21454
|
+
const reference = value["$ref"];
|
|
21455
|
+
if (!reference.startsWith("#/"))
|
|
21456
|
+
return null;
|
|
21457
|
+
let current = document;
|
|
21458
|
+
for (const segment of reference.slice(2).split("/")) {
|
|
21459
|
+
const key = segment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
21460
|
+
if (!current || typeof current !== "object" || Array.isArray(current))
|
|
21461
|
+
return null;
|
|
21462
|
+
current = current[key];
|
|
21463
|
+
}
|
|
21464
|
+
return current && typeof current === "object" && !Array.isArray(current) ? current : null;
|
|
21465
|
+
}
|
|
21466
|
+
function openApiSchemaProperty(document, schemaName, propertyName) {
|
|
21467
|
+
const schema = resolveOpenApiSchema(document, {
|
|
21468
|
+
$ref: `#/components/schemas/${schemaName}`
|
|
21469
|
+
});
|
|
21470
|
+
const properties = schema?.["properties"];
|
|
21471
|
+
if (!properties || typeof properties !== "object" || Array.isArray(properties))
|
|
21472
|
+
return null;
|
|
21473
|
+
const property = properties[propertyName];
|
|
21474
|
+
return property && typeof property === "object" && !Array.isArray(property) ? property : null;
|
|
21475
|
+
}
|
|
21476
|
+
async function fetchTaskCreatorCapability(client) {
|
|
21477
|
+
const document = await requiredRemoteRoute(client, "/v1/openapi.json", () => client.transport.get("/openapi.json"));
|
|
21478
|
+
const inputProperty = openApiSchemaProperty(document, "CreateTaskInput", "created_by");
|
|
21479
|
+
const outputProperty = openApiSchemaProperty(document, "Task", "created_by");
|
|
21480
|
+
return inputProperty?.["type"] === "string" && outputProperty?.["type"] === "string" && outputProperty?.["nullable"] === true;
|
|
21481
|
+
}
|
|
21482
|
+
async function requireTaskCreatorCapability(client) {
|
|
21483
|
+
const authority = remoteAuthorityBase(client);
|
|
21484
|
+
let capability = taskCreatorCapabilityCache.get(authority);
|
|
21485
|
+
if (!capability) {
|
|
21486
|
+
capability = fetchTaskCreatorCapability(client);
|
|
21487
|
+
taskCreatorCapabilityCache.set(authority, capability);
|
|
21488
|
+
}
|
|
21489
|
+
if (!await capability) {
|
|
21490
|
+
throw new Error(`REMOTE_CREATED_BY_UNSUPPORTED: configured Todos authority ${authority} does not advertise created_by ` + "on both CreateTaskInput and Task in /v1/openapi.json; no task mutation was sent; deploy a compatible " + "@hasna/todos /v1 server before retrying; local SQLite fallback is disabled");
|
|
21491
|
+
}
|
|
21492
|
+
}
|
|
21231
21493
|
async function cloudGetStats(client) {
|
|
21232
21494
|
const raw = await requiredRemoteRoute(client, "/v1/stats", () => client.transport.get("/stats"));
|
|
21233
21495
|
return raw ?? {};
|
|
@@ -21423,7 +21685,7 @@ async function cloudResolveTaskListRef(client, ref, projectId) {
|
|
|
21423
21685
|
return input.toLowerCase();
|
|
21424
21686
|
return (await cloudResolveTaskList(client, ref, projectId)).id;
|
|
21425
21687
|
}
|
|
21426
|
-
var UUID_RE, TRANSPORT_TOKENS, completionCapabilityCache, retryCapabilityCache, gitRefCapabilityCache, SERVER_MODE_CANDIDATES, cachedServerMode = null, PRIORITY_RANK, listTagsCapabilityCache;
|
|
21688
|
+
var UUID_RE, TRANSPORT_TOKENS, completionCapabilityCache, retryCapabilityCache, taskCreatorCapabilityCache, gitRefCapabilityCache, remoteCommandCapabilityCache, SERVER_MODE_CANDIDATES, cachedServerMode = null, PRIORITY_RANK, listTagsCapabilityCache;
|
|
21427
21689
|
var init_cloud_router = __esm(() => {
|
|
21428
21690
|
init_types();
|
|
21429
21691
|
init_redaction();
|
|
@@ -21441,7 +21703,9 @@ var init_cloud_router = __esm(() => {
|
|
|
21441
21703
|
};
|
|
21442
21704
|
completionCapabilityCache = new Map;
|
|
21443
21705
|
retryCapabilityCache = new Map;
|
|
21706
|
+
taskCreatorCapabilityCache = new Map;
|
|
21444
21707
|
gitRefCapabilityCache = new Map;
|
|
21708
|
+
remoteCommandCapabilityCache = new Map;
|
|
21445
21709
|
SERVER_MODE_CANDIDATES = ["postgres", "cloud", "self_hosted"];
|
|
21446
21710
|
PRIORITY_RANK = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
21447
21711
|
listTagsCapabilityCache = new Map;
|
|
@@ -21837,7 +22101,7 @@ var init_task_crud2 = __esm(() => {
|
|
|
21837
22101
|
});
|
|
21838
22102
|
|
|
21839
22103
|
// src/lib/project-bootstrap.ts
|
|
21840
|
-
import { existsSync as existsSync8, readFileSync as
|
|
22104
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5, statSync as statSync3 } from "fs";
|
|
21841
22105
|
import { basename as basename2, dirname as dirname5, resolve as resolve8 } from "path";
|
|
21842
22106
|
function safeStat(path) {
|
|
21843
22107
|
try {
|
|
@@ -21871,7 +22135,7 @@ function readPackageJson(path) {
|
|
|
21871
22135
|
if (!existsSync8(file))
|
|
21872
22136
|
return null;
|
|
21873
22137
|
try {
|
|
21874
|
-
const parsed = JSON.parse(
|
|
22138
|
+
const parsed = JSON.parse(readFileSync5(file, "utf-8"));
|
|
21875
22139
|
return parsed && typeof parsed === "object" ? parsed : null;
|
|
21876
22140
|
} catch {
|
|
21877
22141
|
return null;
|
|
@@ -22433,7 +22697,7 @@ var init_retention_cleanup = __esm(() => {
|
|
|
22433
22697
|
});
|
|
22434
22698
|
|
|
22435
22699
|
// src/lib/mention-resolver.ts
|
|
22436
|
-
import { existsSync as existsSync10, readdirSync as readdirSync2, readFileSync as
|
|
22700
|
+
import { existsSync as existsSync10, readdirSync as readdirSync2, readFileSync as readFileSync6, statSync as statSync4 } from "fs";
|
|
22437
22701
|
import { basename as basename3, isAbsolute, join as join8, relative as relative3, resolve as resolve9, sep as sep2 } from "path";
|
|
22438
22702
|
function blankResolution(parsed) {
|
|
22439
22703
|
return {
|
|
@@ -22542,7 +22806,7 @@ function resolveFile(parsed, workspace) {
|
|
|
22542
22806
|
return resolution;
|
|
22543
22807
|
}
|
|
22544
22808
|
if (parsed.line !== undefined) {
|
|
22545
|
-
const lineCount =
|
|
22809
|
+
const lineCount = readFileSync6(absolutePath, "utf-8").split(/\r?\n/).length;
|
|
22546
22810
|
if (parsed.line < 1 || parsed.line > lineCount) {
|
|
22547
22811
|
resolution.warnings.push(`line ${parsed.line} is outside the file range 1-${lineCount}`);
|
|
22548
22812
|
return resolution;
|
|
@@ -22595,7 +22859,7 @@ function resolveSymbol(parsed, workspace, maxMatches) {
|
|
|
22595
22859
|
const pattern = symbolPattern(name);
|
|
22596
22860
|
const matches = [];
|
|
22597
22861
|
for (const file of walkSourceFiles(workspace)) {
|
|
22598
|
-
const lines =
|
|
22862
|
+
const lines = readFileSync6(file, "utf-8").split(/\r?\n/);
|
|
22599
22863
|
for (let index = 0;index < lines.length; index += 1) {
|
|
22600
22864
|
const line = lines[index];
|
|
22601
22865
|
const found = pattern.exec(line);
|
|
@@ -26262,7 +26526,7 @@ var init_audit_ledger = __esm(() => {
|
|
|
26262
26526
|
});
|
|
26263
26527
|
|
|
26264
26528
|
// src/lib/release-compatibility.ts
|
|
26265
|
-
import { readFileSync as
|
|
26529
|
+
import { readFileSync as readFileSync7 } from "fs";
|
|
26266
26530
|
import { join as join9, resolve as resolve11 } from "path";
|
|
26267
26531
|
import { Database as Database2 } from "bun:sqlite";
|
|
26268
26532
|
function pass(id, message, details) {
|
|
@@ -26275,7 +26539,7 @@ function warn(id, message, details) {
|
|
|
26275
26539
|
return { id, status: "warning", message, details };
|
|
26276
26540
|
}
|
|
26277
26541
|
function readPackageJson2(root) {
|
|
26278
|
-
return JSON.parse(
|
|
26542
|
+
return JSON.parse(readFileSync7(join9(root, "package.json"), "utf8"));
|
|
26279
26543
|
}
|
|
26280
26544
|
function sortedKeys(value) {
|
|
26281
26545
|
return Object.keys(value ?? {}).sort((left, right) => left.localeCompare(right));
|
|
@@ -26815,6 +27079,8 @@ function searchTaskEntities(filters, db) {
|
|
|
26815
27079
|
return tasks.filter((task) => taskMatchesSavedFilters(task, filters, db)).slice(0, normalizeLimit(filters.limit));
|
|
26816
27080
|
}
|
|
26817
27081
|
function searchProjects(filters, db) {
|
|
27082
|
+
if (filters.agent_id)
|
|
27083
|
+
return [];
|
|
26818
27084
|
const params = [];
|
|
26819
27085
|
let sql = "SELECT * FROM projects WHERE 1=1";
|
|
26820
27086
|
if (filters.project_id) {
|
|
@@ -33215,7 +33481,7 @@ var init_agent_run_dispatcher = __esm(() => {
|
|
|
33215
33481
|
});
|
|
33216
33482
|
|
|
33217
33483
|
// src/lib/verification-providers.ts
|
|
33218
|
-
import { existsSync as existsSync12, readFileSync as
|
|
33484
|
+
import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
|
|
33219
33485
|
function normalizeName5(name) {
|
|
33220
33486
|
const normalized = name.trim().toLowerCase();
|
|
33221
33487
|
if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(normalized)) {
|
|
@@ -33367,7 +33633,7 @@ Timed out after ${provider.timeout_ms}ms`);
|
|
|
33367
33633
|
};
|
|
33368
33634
|
}
|
|
33369
33635
|
function runCiLogProvider(input) {
|
|
33370
|
-
const text = input.log_text ?? (input.log_path && existsSync12(input.log_path) ?
|
|
33636
|
+
const text = input.log_text ?? (input.log_path && existsSync12(input.log_path) ? readFileSync8(input.log_path, "utf-8") : "");
|
|
33371
33637
|
return {
|
|
33372
33638
|
status: classifyLog(text),
|
|
33373
33639
|
attempts: 1,
|
|
@@ -35537,7 +35803,7 @@ var package_default;
|
|
|
35537
35803
|
var init_package = __esm(() => {
|
|
35538
35804
|
package_default = {
|
|
35539
35805
|
name: "@hasna/todos",
|
|
35540
|
-
version: "0.15.
|
|
35806
|
+
version: "0.15.24",
|
|
35541
35807
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
35542
35808
|
type: "module",
|
|
35543
35809
|
main: "dist/index.js",
|
|
@@ -35608,6 +35874,8 @@ var init_package = __esm(() => {
|
|
|
35608
35874
|
"dev:mcp": "bun run src/mcp/index.ts",
|
|
35609
35875
|
"dev:serve": "bun run src/server/index.ts",
|
|
35610
35876
|
"verify:release": "bun run scripts/verify-public-release.ts --mode=review",
|
|
35877
|
+
"verify:release-review": "bun run scripts/verify-npm-release-agent-review.ts",
|
|
35878
|
+
"issue:release-review": "bun run scripts/issue-npm-release-agent-review.ts",
|
|
35611
35879
|
prepublishOnly: "bun run scripts/verify-public-release.ts --mode=publish",
|
|
35612
35880
|
postinstall: "mkdir -p $HOME/.hasna/todos $HOME/.hasna/todos/training 2>/dev/null || true"
|
|
35613
35881
|
},
|
|
@@ -36252,7 +36520,7 @@ var init_local_bridge = __esm(() => {
|
|
|
36252
36520
|
|
|
36253
36521
|
// src/lib/local-backups.ts
|
|
36254
36522
|
import { createHash as createHash7 } from "crypto";
|
|
36255
|
-
import { readFileSync as
|
|
36523
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync3 } from "fs";
|
|
36256
36524
|
import { dirname as dirname7, resolve as resolve12 } from "path";
|
|
36257
36525
|
import { mkdirSync as mkdirSync6 } from "fs";
|
|
36258
36526
|
function stableJson2(value) {
|
|
@@ -36362,7 +36630,7 @@ function writeLocalBackupFile(backup, outputPath) {
|
|
|
36362
36630
|
return path;
|
|
36363
36631
|
}
|
|
36364
36632
|
function readLocalBackupFile(path) {
|
|
36365
|
-
return JSON.parse(
|
|
36633
|
+
return JSON.parse(readFileSync9(resolve12(path), "utf-8"));
|
|
36366
36634
|
}
|
|
36367
36635
|
function verifyLocalBackup(value, options = {}, db) {
|
|
36368
36636
|
const verifiedAt = options.verified_at ?? now();
|
|
@@ -37813,7 +38081,7 @@ __export(exports_local_extensions, {
|
|
|
37813
38081
|
discoverLocalExtensions: () => discoverLocalExtensions
|
|
37814
38082
|
});
|
|
37815
38083
|
import { createHash as createHash10, createVerify } from "crypto";
|
|
37816
|
-
import { existsSync as existsSync13, readdirSync as readdirSync3, readFileSync as
|
|
38084
|
+
import { existsSync as existsSync13, readdirSync as readdirSync3, readFileSync as readFileSync10, statSync as statSync6 } from "fs";
|
|
37817
38085
|
import { basename as basename5, join as join11, resolve as resolve13 } from "path";
|
|
37818
38086
|
function isObject2(value) {
|
|
37819
38087
|
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
@@ -37895,7 +38163,7 @@ function normalizeManifest(input) {
|
|
|
37895
38163
|
};
|
|
37896
38164
|
}
|
|
37897
38165
|
function parseJson(path) {
|
|
37898
|
-
return JSON.parse(
|
|
38166
|
+
return JSON.parse(readFileSync10(path, "utf8"));
|
|
37899
38167
|
}
|
|
37900
38168
|
function sha2566(bytes) {
|
|
37901
38169
|
return `sha256:${createHash10("sha256").update(bytes).digest("hex")}`;
|
|
@@ -38079,7 +38347,7 @@ function inspectExtensionSource(source3) {
|
|
|
38079
38347
|
const manifestPath = stat.isDirectory() ? [join11(resolved, "todos.extension.json"), join11(resolved, "extension.json")].find(existsSync13) : resolved;
|
|
38080
38348
|
if (!manifestPath)
|
|
38081
38349
|
throw new Error(`extension directory ${source3} is missing todos.extension.json`);
|
|
38082
|
-
const raw =
|
|
38350
|
+
const raw = readFileSync10(manifestPath);
|
|
38083
38351
|
const parsed = parseJson(manifestPath);
|
|
38084
38352
|
const bundle = isObject2(parsed) && isObject2(parsed["manifest"]);
|
|
38085
38353
|
const manifest = normalizeManifest(bundle ? parsed["manifest"] : parsed);
|
|
@@ -42592,7 +42860,7 @@ __export(exports_extract, {
|
|
|
42592
42860
|
buildCodebaseIndex: () => buildCodebaseIndex,
|
|
42593
42861
|
EXTRACT_TAGS: () => EXTRACT_TAGS
|
|
42594
42862
|
});
|
|
42595
|
-
import { existsSync as existsSync14, readFileSync as
|
|
42863
|
+
import { existsSync as existsSync14, readFileSync as readFileSync11, statSync as statSync7 } from "fs";
|
|
42596
42864
|
import { createHash as createHash12 } from "crypto";
|
|
42597
42865
|
import { relative as relative5, resolve as resolve14, join as join12 } from "path";
|
|
42598
42866
|
function stableHash(value) {
|
|
@@ -42607,7 +42875,7 @@ function readGitignorePatterns(basePath) {
|
|
|
42607
42875
|
if (!existsSync14(gitignorePath))
|
|
42608
42876
|
return [];
|
|
42609
42877
|
try {
|
|
42610
|
-
return
|
|
42878
|
+
return readFileSync11(gitignorePath, "utf-8").split(`
|
|
42611
42879
|
`).map((line) => line.trim()).filter((line) => line && !line.startsWith("#") && !line.startsWith("!"));
|
|
42612
42880
|
} catch {
|
|
42613
42881
|
return [];
|
|
@@ -42748,7 +43016,7 @@ function buildCodebaseIndex(options) {
|
|
|
42748
43016
|
for (const file of files) {
|
|
42749
43017
|
const fullPath = statSync7(basePath).isFile() ? basePath : join12(basePath, file);
|
|
42750
43018
|
try {
|
|
42751
|
-
const source3 =
|
|
43019
|
+
const source3 = readFileSync11(fullPath, "utf-8");
|
|
42752
43020
|
const relPath = statSync7(basePath).isFile() ? relative5(resolve14(basePath, ".."), fullPath) : file;
|
|
42753
43021
|
indexed.push({
|
|
42754
43022
|
file: relPath,
|
|
@@ -42779,7 +43047,7 @@ function extractTodos(options, db) {
|
|
|
42779
43047
|
for (const file of files) {
|
|
42780
43048
|
const fullPath = statSync7(basePath).isFile() ? basePath : join12(basePath, file);
|
|
42781
43049
|
try {
|
|
42782
|
-
const source3 =
|
|
43050
|
+
const source3 = readFileSync11(fullPath, "utf-8");
|
|
42783
43051
|
const relPath = statSync7(basePath).isFile() ? relative5(resolve14(basePath, ".."), fullPath) : file;
|
|
42784
43052
|
const comments = extractFromSource(source3, relPath, tags);
|
|
42785
43053
|
allComments.push(...comments);
|
|
@@ -44321,7 +44589,7 @@ __export(exports_environment_snapshots, {
|
|
|
44321
44589
|
captureEnvironmentSnapshot: () => captureEnvironmentSnapshot
|
|
44322
44590
|
});
|
|
44323
44591
|
import { createHash as createHash13 } from "crypto";
|
|
44324
|
-
import { existsSync as existsSync15, readFileSync as
|
|
44592
|
+
import { existsSync as existsSync15, readFileSync as readFileSync12, statSync as statSync8 } from "fs";
|
|
44325
44593
|
import { hostname as hostname2, platform, arch } from "os";
|
|
44326
44594
|
import { dirname as dirname8, join as join14, resolve as resolve15 } from "path";
|
|
44327
44595
|
import { tmpdir as tmpdir3 } from "os";
|
|
@@ -44335,7 +44603,7 @@ function fileRecord(root, relativePath) {
|
|
|
44335
44603
|
const stat = statSync8(path);
|
|
44336
44604
|
if (!stat.isFile())
|
|
44337
44605
|
return null;
|
|
44338
|
-
const content =
|
|
44606
|
+
const content = readFileSync12(path);
|
|
44339
44607
|
return { path: relativePath, sha256: sha2567(content), size_bytes: content.length };
|
|
44340
44608
|
}
|
|
44341
44609
|
function manifestRecord(root, relativePath) {
|
|
@@ -45098,6 +45366,58 @@ var init_plan_project_links = __esm(() => {
|
|
|
45098
45366
|
init_tasks();
|
|
45099
45367
|
});
|
|
45100
45368
|
|
|
45369
|
+
// src/storage/audit-history-import.ts
|
|
45370
|
+
function auditHistoryRowsAreFieldIdentical(left, right) {
|
|
45371
|
+
return AUDIT_HISTORY_FIELDS.every((field) => {
|
|
45372
|
+
const leftValue = field === "machine_id" ? left[field] ?? null : left[field];
|
|
45373
|
+
const rightValue = field === "machine_id" ? right[field] ?? null : right[field];
|
|
45374
|
+
return leftValue === rightValue;
|
|
45375
|
+
});
|
|
45376
|
+
}
|
|
45377
|
+
function divergentAuditHistoryReplayError(id) {
|
|
45378
|
+
return `${AUDIT_HISTORY_DIVERGENT_REPLAY}: immutable audit_history row ${id} differs from stored row`;
|
|
45379
|
+
}
|
|
45380
|
+
function forbiddenAuditHistoryTombstoneError(id) {
|
|
45381
|
+
return `${AUDIT_HISTORY_TOMBSTONE_FORBIDDEN}: audit_history tombstone ${id} is not allowed`;
|
|
45382
|
+
}
|
|
45383
|
+
function parseAuditHistoryImportFailure(message) {
|
|
45384
|
+
const divergentPrefix = `${AUDIT_HISTORY_DIVERGENT_REPLAY}: immutable audit_history row `;
|
|
45385
|
+
const divergentSuffix = " differs from stored row";
|
|
45386
|
+
if (message.startsWith(divergentPrefix) && message.endsWith(divergentSuffix)) {
|
|
45387
|
+
return {
|
|
45388
|
+
code: AUDIT_HISTORY_DIVERGENT_REPLAY,
|
|
45389
|
+
auditHistoryId: message.slice(divergentPrefix.length, -divergentSuffix.length),
|
|
45390
|
+
conflict: true,
|
|
45391
|
+
status: 409
|
|
45392
|
+
};
|
|
45393
|
+
}
|
|
45394
|
+
const tombstonePrefix = `${AUDIT_HISTORY_TOMBSTONE_FORBIDDEN}: audit_history tombstone `;
|
|
45395
|
+
const tombstoneSuffix = " is not allowed";
|
|
45396
|
+
if (message.startsWith(tombstonePrefix) && message.endsWith(tombstoneSuffix)) {
|
|
45397
|
+
return {
|
|
45398
|
+
code: AUDIT_HISTORY_TOMBSTONE_FORBIDDEN,
|
|
45399
|
+
auditHistoryId: message.slice(tombstonePrefix.length, -tombstoneSuffix.length),
|
|
45400
|
+
conflict: false,
|
|
45401
|
+
status: 400
|
|
45402
|
+
};
|
|
45403
|
+
}
|
|
45404
|
+
return null;
|
|
45405
|
+
}
|
|
45406
|
+
var AUDIT_HISTORY_DIVERGENT_REPLAY = "AUDIT_HISTORY_DIVERGENT_REPLAY", AUDIT_HISTORY_TOMBSTONE_FORBIDDEN = "AUDIT_HISTORY_TOMBSTONE_FORBIDDEN", AUDIT_HISTORY_FIELDS;
|
|
45407
|
+
var init_audit_history_import = __esm(() => {
|
|
45408
|
+
AUDIT_HISTORY_FIELDS = [
|
|
45409
|
+
"id",
|
|
45410
|
+
"task_id",
|
|
45411
|
+
"action",
|
|
45412
|
+
"field",
|
|
45413
|
+
"old_value",
|
|
45414
|
+
"new_value",
|
|
45415
|
+
"agent_id",
|
|
45416
|
+
"created_at",
|
|
45417
|
+
"machine_id"
|
|
45418
|
+
];
|
|
45419
|
+
});
|
|
45420
|
+
|
|
45101
45421
|
// src/storage/sqlite-snapshot.ts
|
|
45102
45422
|
function exportSqliteTodosStorageSnapshot(db) {
|
|
45103
45423
|
const d = db ?? getDatabase();
|
|
@@ -45131,8 +45451,11 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
45131
45451
|
const existingTaskLists = d.query("SELECT id, project_id, slug FROM task_lists").all();
|
|
45132
45452
|
result.errors.push(...validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists));
|
|
45133
45453
|
}
|
|
45454
|
+
const auditImport = preflightAuditHistoryImport(d, snapshot.auditHistory, snapshot.tombstones ?? []);
|
|
45455
|
+
result.errors.push(...auditImport.errors);
|
|
45134
45456
|
if (result.errors.length > 0)
|
|
45135
45457
|
return result;
|
|
45458
|
+
result.skipped += auditImport.identicalReplayCount;
|
|
45136
45459
|
const applyRows = (objectType2, table, columns, rows, updateClockColumn, acceptEqualClock = true, afterUpsert) => {
|
|
45137
45460
|
for (const row of rows) {
|
|
45138
45461
|
try {
|
|
@@ -45167,10 +45490,72 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
|
|
|
45167
45490
|
replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
|
|
45168
45491
|
}
|
|
45169
45492
|
});
|
|
45170
|
-
|
|
45493
|
+
insertAuditHistoryRows(d, auditImport.rowsToInsert, result);
|
|
45171
45494
|
applyTombstones(d, snapshot.tombstones ?? [], result);
|
|
45172
45495
|
return result;
|
|
45173
45496
|
}
|
|
45497
|
+
function preflightAuditHistoryImport(db, rows, tombstones) {
|
|
45498
|
+
const errors2 = tombstones.filter((tombstone) => tombstone.object_type === "audit_history").map((tombstone) => forbiddenAuditHistoryTombstoneError(tombstone.object_id));
|
|
45499
|
+
const rowsToInsert = [];
|
|
45500
|
+
const seen = new Map;
|
|
45501
|
+
let identicalReplayCount = 0;
|
|
45502
|
+
for (const rawRow of rows) {
|
|
45503
|
+
try {
|
|
45504
|
+
const row = asRecord2(rawRow);
|
|
45505
|
+
if (typeof row.id !== "string" || !row.id) {
|
|
45506
|
+
throw new Error("task_history row is missing id");
|
|
45507
|
+
}
|
|
45508
|
+
const prior = seen.get(row.id);
|
|
45509
|
+
if (prior) {
|
|
45510
|
+
if (auditHistoryRowsAreFieldIdentical(prior, row))
|
|
45511
|
+
identicalReplayCount += 1;
|
|
45512
|
+
else
|
|
45513
|
+
errors2.push(divergentAuditHistoryReplayError(row.id));
|
|
45514
|
+
continue;
|
|
45515
|
+
}
|
|
45516
|
+
seen.set(row.id, row);
|
|
45517
|
+
const existing = getAuditHistoryById(db, row.id);
|
|
45518
|
+
if (!existing) {
|
|
45519
|
+
rowsToInsert.push(row);
|
|
45520
|
+
} else if (auditHistoryRowsAreFieldIdentical(existing, row)) {
|
|
45521
|
+
identicalReplayCount += 1;
|
|
45522
|
+
} else {
|
|
45523
|
+
errors2.push(divergentAuditHistoryReplayError(row.id));
|
|
45524
|
+
}
|
|
45525
|
+
} catch (error) {
|
|
45526
|
+
errors2.push(error instanceof Error ? error.message : String(error));
|
|
45527
|
+
}
|
|
45528
|
+
}
|
|
45529
|
+
return { rowsToInsert, identicalReplayCount, errors: errors2 };
|
|
45530
|
+
}
|
|
45531
|
+
function insertAuditHistoryRows(db, rows, result) {
|
|
45532
|
+
for (const rawRow of rows) {
|
|
45533
|
+
try {
|
|
45534
|
+
const row = asRecord2(rawRow);
|
|
45535
|
+
const presentColumns = AUDIT_COLUMNS.filter((column) => (column in row));
|
|
45536
|
+
if (!presentColumns.includes("id"))
|
|
45537
|
+
presentColumns.unshift("id");
|
|
45538
|
+
const placeholders2 = presentColumns.map(() => "?").join(", ");
|
|
45539
|
+
const values = presentColumns.map((column) => valueForColumn(column, row[column]));
|
|
45540
|
+
const changes = db.run(`INSERT OR IGNORE INTO task_history (${presentColumns.join(", ")}) VALUES (${placeholders2})`, values).changes;
|
|
45541
|
+
if (changes > 0) {
|
|
45542
|
+
result.inserted += 1;
|
|
45543
|
+
continue;
|
|
45544
|
+
}
|
|
45545
|
+
const existing = getAuditHistoryById(db, String(row["id"]));
|
|
45546
|
+
if (existing && auditHistoryRowsAreFieldIdentical(existing, row)) {
|
|
45547
|
+
result.skipped += 1;
|
|
45548
|
+
} else {
|
|
45549
|
+
result.errors.push(divergentAuditHistoryReplayError(String(row["id"])));
|
|
45550
|
+
}
|
|
45551
|
+
} catch (error) {
|
|
45552
|
+
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
45553
|
+
}
|
|
45554
|
+
}
|
|
45555
|
+
}
|
|
45556
|
+
function getAuditHistoryById(db, id) {
|
|
45557
|
+
return db.query(`SELECT ${AUDIT_COLUMNS.join(", ")} FROM task_history WHERE id = ? LIMIT 1`).get(id);
|
|
45558
|
+
}
|
|
45174
45559
|
function upsertById(db, table, columns, row, updateClockColumn, acceptEqualClock = true) {
|
|
45175
45560
|
const id = row["id"];
|
|
45176
45561
|
if (typeof id !== "string" || !id)
|
|
@@ -45268,7 +45653,7 @@ function tableForTombstone(objectType2) {
|
|
|
45268
45653
|
return "task_templates";
|
|
45269
45654
|
if (objectType2 === "template_tasks")
|
|
45270
45655
|
return "template_tasks";
|
|
45271
|
-
|
|
45656
|
+
throw new Error(`unsupported storage tombstone object_type: ${String(objectType2)}`);
|
|
45272
45657
|
}
|
|
45273
45658
|
function listRows(db, table, columns) {
|
|
45274
45659
|
return db.query(`SELECT ${columns.join(", ")} FROM ${table} ORDER BY id`).all();
|
|
@@ -45315,6 +45700,7 @@ var init_sqlite_snapshot = __esm(() => {
|
|
|
45315
45700
|
init_tasks();
|
|
45316
45701
|
init_templates();
|
|
45317
45702
|
init_storage_tombstones();
|
|
45703
|
+
init_audit_history_import();
|
|
45318
45704
|
PROJECT_COLUMNS = [
|
|
45319
45705
|
"id",
|
|
45320
45706
|
"name",
|
|
@@ -45579,6 +45965,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
|
|
|
45579
45965
|
unlockTask(id, agentId, database());
|
|
45580
45966
|
return true;
|
|
45581
45967
|
},
|
|
45968
|
+
handoffStaleLock: (input) => handoffStaleTaskLock(input, database()),
|
|
45582
45969
|
delete: (id) => deleteTask(id, database()),
|
|
45583
45970
|
start: (id, agentId) => startTask(id, agentId, database()),
|
|
45584
45971
|
complete: (id, agentId, options2) => completeTask(id, agentId, database(), options2),
|
|
@@ -46497,8 +46884,33 @@ var init_api_keys = __esm(() => {
|
|
|
46497
46884
|
init_database();
|
|
46498
46885
|
});
|
|
46499
46886
|
|
|
46887
|
+
// src/task-manifest/canonical.ts
|
|
46888
|
+
import { createHash as createHash15 } from "crypto";
|
|
46889
|
+
function canonicalize2(value) {
|
|
46890
|
+
if (Array.isArray(value))
|
|
46891
|
+
return value.map(canonicalize2);
|
|
46892
|
+
if (value !== null && typeof value === "object") {
|
|
46893
|
+
return Object.fromEntries(Object.entries(value).filter(([, entry2]) => entry2 !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry2]) => [key, canonicalize2(entry2)]));
|
|
46894
|
+
}
|
|
46895
|
+
return value;
|
|
46896
|
+
}
|
|
46897
|
+
function canonicalJson(value) {
|
|
46898
|
+
return JSON.stringify(canonicalize2(value));
|
|
46899
|
+
}
|
|
46900
|
+
function canonicalDigest(value) {
|
|
46901
|
+
return createHash15("sha256").update(canonicalJson(value)).digest("hex");
|
|
46902
|
+
}
|
|
46903
|
+
function deterministicUuid(namespace, ...parts) {
|
|
46904
|
+
const bytes = createHash15("sha256").update([namespace, ...parts].join("\x1F")).digest().subarray(0, 16);
|
|
46905
|
+
bytes[6] = bytes[6] & 15 | 80;
|
|
46906
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
46907
|
+
const hex = bytes.toString("hex");
|
|
46908
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
46909
|
+
}
|
|
46910
|
+
var init_canonical = () => {};
|
|
46911
|
+
|
|
46500
46912
|
// src/storage/postgres-adapter.ts
|
|
46501
|
-
import { randomUUID as
|
|
46913
|
+
import { randomUUID as randomUUID4 } from "crypto";
|
|
46502
46914
|
function createPostgresTodosStorageAdapter(options) {
|
|
46503
46915
|
const store = new PostgresJsonRecordStore(options);
|
|
46504
46916
|
const adapter = {
|
|
@@ -46527,6 +46939,7 @@ function createPostgresTodosStorageAdapter(options) {
|
|
|
46527
46939
|
getChangedSince: (since, filters) => getChangedSince(since, filters, store),
|
|
46528
46940
|
lock: (id, agentId) => lockTask2(id, agentId, store),
|
|
46529
46941
|
unlock: (id, agentId) => unlockTask2(id, agentId, store),
|
|
46942
|
+
handoffStaleLock: (input, context) => store.handoffStaleLock(input, context),
|
|
46530
46943
|
getByFingerprint: (fingerprint3) => store.getTaskByFingerprint(fingerprint3)
|
|
46531
46944
|
},
|
|
46532
46945
|
dependencies: {
|
|
@@ -46673,6 +47086,91 @@ class PostgresJsonRecordStore {
|
|
|
46673
47086
|
LIMIT 1`, [this.service, type, id]);
|
|
46674
47087
|
return result.rows[0] ? payloadRecord2(result.rows[0].payload) : null;
|
|
46675
47088
|
}
|
|
47089
|
+
async handoffStaleLock(input, context = {}) {
|
|
47090
|
+
const prepared = prepareStaleLockHandoff(input);
|
|
47091
|
+
const receipt = buildStaleLockHandoffReceipt(prepared);
|
|
47092
|
+
const history = staleLockHandoffHistory(receipt, this.machineId(context));
|
|
47093
|
+
await this.ensureSchema();
|
|
47094
|
+
const result = await this.options.client.query(`/* todos:stale-lock-handoff-atomic */ WITH
|
|
47095
|
+
target AS MATERIALIZED (
|
|
47096
|
+
SELECT payload
|
|
47097
|
+
FROM ${this.tableName}
|
|
47098
|
+
WHERE service = $1
|
|
47099
|
+
AND object_type = 'tasks'
|
|
47100
|
+
AND object_id = $2
|
|
47101
|
+
AND deleted_at IS NULL
|
|
47102
|
+
FOR UPDATE
|
|
47103
|
+
),
|
|
47104
|
+
updated AS (
|
|
47105
|
+
UPDATE ${this.tableName} AS task_record
|
|
47106
|
+
SET payload = jsonb_set(
|
|
47107
|
+
jsonb_set(
|
|
47108
|
+
jsonb_set(
|
|
47109
|
+
jsonb_set(
|
|
47110
|
+
task_record.payload,
|
|
47111
|
+
'{locked_by}',
|
|
47112
|
+
to_jsonb($6::text),
|
|
47113
|
+
true
|
|
47114
|
+
),
|
|
47115
|
+
'{locked_at}',
|
|
47116
|
+
to_jsonb($7::text),
|
|
47117
|
+
true
|
|
47118
|
+
),
|
|
47119
|
+
'{updated_at}',
|
|
47120
|
+
to_jsonb($7::text),
|
|
47121
|
+
true
|
|
47122
|
+
),
|
|
47123
|
+
'{version}',
|
|
47124
|
+
to_jsonb(COALESCE((task_record.payload->>'version')::integer, 0) + 1),
|
|
47125
|
+
true
|
|
47126
|
+
),
|
|
47127
|
+
updated_at = $7::timestamptz,
|
|
47128
|
+
source_machine_id = $10,
|
|
47129
|
+
version = COALESCE(task_record.version, 0) + 1
|
|
47130
|
+
FROM target
|
|
47131
|
+
WHERE task_record.service = $1
|
|
47132
|
+
AND task_record.object_type = 'tasks'
|
|
47133
|
+
AND task_record.object_id = $2
|
|
47134
|
+
AND task_record.deleted_at IS NULL
|
|
47135
|
+
AND target.payload->>'locked_by' = $3
|
|
47136
|
+
AND target.payload->>'locked_at' = $4
|
|
47137
|
+
AND $4::timestamptz < $5::timestamptz
|
|
47138
|
+
AND COALESCE(target.payload->>'status', '') NOT IN ('completed', 'failed', 'cancelled')
|
|
47139
|
+
RETURNING task_record.payload
|
|
47140
|
+
),
|
|
47141
|
+
audit AS (
|
|
47142
|
+
INSERT INTO ${this.tableName} (
|
|
47143
|
+
service, object_type, object_id, payload, updated_at,
|
|
47144
|
+
deleted_at, source_machine_id, version
|
|
47145
|
+
)
|
|
47146
|
+
SELECT $1, 'audit_history', $8, $9::jsonb, $7::timestamptz,
|
|
47147
|
+
NULL, $10, NULL
|
|
47148
|
+
FROM updated
|
|
47149
|
+
RETURNING payload
|
|
47150
|
+
)
|
|
47151
|
+
SELECT
|
|
47152
|
+
(SELECT payload FROM target) AS current_payload,
|
|
47153
|
+
(SELECT payload FROM updated) AS updated_payload,
|
|
47154
|
+
(SELECT payload FROM audit) AS audit_payload`, [
|
|
47155
|
+
this.service,
|
|
47156
|
+
prepared.task_id,
|
|
47157
|
+
prepared.expected_holder,
|
|
47158
|
+
prepared.expected_lock_version,
|
|
47159
|
+
prepared.stale_cutoff,
|
|
47160
|
+
prepared.new_holder,
|
|
47161
|
+
prepared.operation_timestamp,
|
|
47162
|
+
receipt.receipt_id,
|
|
47163
|
+
jsonbParam(history),
|
|
47164
|
+
this.machineId(context)
|
|
47165
|
+
]);
|
|
47166
|
+
const row = result.rows[0];
|
|
47167
|
+
if (!row?.current_payload)
|
|
47168
|
+
throw new TaskNotFoundError(prepared.task_id);
|
|
47169
|
+
if (!row.updated_payload || !row.audit_payload) {
|
|
47170
|
+
throwStaleLockHandoffConflict(payloadRecord2(row.current_payload), prepared);
|
|
47171
|
+
}
|
|
47172
|
+
return receipt;
|
|
47173
|
+
}
|
|
46676
47174
|
async list(type) {
|
|
46677
47175
|
return (await this.listRecords(type)).map((record) => record.payload);
|
|
46678
47176
|
}
|
|
@@ -46942,6 +47440,28 @@ class PostgresJsonRecordStore {
|
|
|
46942
47440
|
}
|
|
46943
47441
|
return value;
|
|
46944
47442
|
}
|
|
47443
|
+
async insertImmutableAuditHistory(value, context = {}) {
|
|
47444
|
+
await this.ensureSchema();
|
|
47445
|
+
const inserted = await this.options.client.query(`INSERT INTO ${this.tableName} (
|
|
47446
|
+
service, object_type, object_id, payload, updated_at,
|
|
47447
|
+
deleted_at, source_machine_id, version
|
|
47448
|
+
) VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, NULL, $6, NULL)
|
|
47449
|
+
ON CONFLICT (service, object_type, object_id) DO NOTHING
|
|
47450
|
+
RETURNING object_id`, [
|
|
47451
|
+
this.service,
|
|
47452
|
+
"audit_history",
|
|
47453
|
+
value.id,
|
|
47454
|
+
jsonbParam(value),
|
|
47455
|
+
value.created_at,
|
|
47456
|
+
context.requestId ?? this.sourceMachineId ?? null
|
|
47457
|
+
]);
|
|
47458
|
+
if (inserted.rows.length > 0)
|
|
47459
|
+
return "inserted";
|
|
47460
|
+
const existing = await this.get("audit_history", value.id);
|
|
47461
|
+
if (existing && auditHistoryRowsAreFieldIdentical(existing, value))
|
|
47462
|
+
return "identical";
|
|
47463
|
+
throw new Error(divergentAuditHistoryReplayError(value.id));
|
|
47464
|
+
}
|
|
46945
47465
|
async upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context = {}) {
|
|
46946
47466
|
const planIds = [...new Set(guardedPlanIds.filter(Boolean))].sort();
|
|
46947
47467
|
if (planIds.length === 0)
|
|
@@ -47768,7 +48288,7 @@ async function createTask3(input, store, context) {
|
|
|
47768
48288
|
const effectiveProjectId = linkedPlan?.project_id ?? requestedProjectId;
|
|
47769
48289
|
const shortId = effectiveProjectId ? await nextTaskShortId2(effectiveProjectId, store, context) : null;
|
|
47770
48290
|
const task2 = {
|
|
47771
|
-
id:
|
|
48291
|
+
id: randomUUID4(),
|
|
47772
48292
|
short_id: shortId,
|
|
47773
48293
|
project_id: effectiveProjectId,
|
|
47774
48294
|
parent_id: input.parent_id ?? null,
|
|
@@ -48014,7 +48534,7 @@ async function addVerification(input, store, context) {
|
|
|
48014
48534
|
throw new Error(`Task not found: ${input.task_id}`);
|
|
48015
48535
|
const timestamp4 = new Date().toISOString();
|
|
48016
48536
|
const verification = {
|
|
48017
|
-
id:
|
|
48537
|
+
id: randomUUID4(),
|
|
48018
48538
|
task_id: input.task_id,
|
|
48019
48539
|
command: input.command,
|
|
48020
48540
|
status: input.status ?? "unknown",
|
|
@@ -48035,7 +48555,7 @@ async function addCommit(input, store, context) {
|
|
|
48035
48555
|
throw new Error(`Task not found: ${input.task_id}`);
|
|
48036
48556
|
const timestamp4 = new Date().toISOString();
|
|
48037
48557
|
const commit = {
|
|
48038
|
-
id:
|
|
48558
|
+
id: randomUUID4(),
|
|
48039
48559
|
task_id: input.task_id,
|
|
48040
48560
|
sha: input.sha,
|
|
48041
48561
|
message: input.message ?? null,
|
|
@@ -48056,16 +48576,17 @@ async function findCommit(sha, store) {
|
|
|
48056
48576
|
async function addGitRef(input, store, context) {
|
|
48057
48577
|
if (!await store.get("tasks", input.task_id))
|
|
48058
48578
|
throw new Error(`Task not found: ${input.task_id}`);
|
|
48579
|
+
const existing = (await store.list("refs")).filter((ref) => ref.task_id === input.task_id && ref.ref_type === input.ref_type && ref.name === input.name).sort((left, right) => left.created_at.localeCompare(right.created_at) || left.id.localeCompare(right.id))[0];
|
|
48059
48580
|
const timestamp4 = new Date().toISOString();
|
|
48060
48581
|
const gitRef = {
|
|
48061
|
-
id:
|
|
48582
|
+
id: existing?.id ?? deterministicUuid("todos:git-ref:v1", input.task_id, input.ref_type, input.name),
|
|
48062
48583
|
task_id: input.task_id,
|
|
48063
48584
|
ref_type: input.ref_type,
|
|
48064
48585
|
name: input.name,
|
|
48065
|
-
url: input.url ?? null,
|
|
48066
|
-
provider: input.provider ?? null,
|
|
48586
|
+
url: input.url ?? existing?.url ?? null,
|
|
48587
|
+
provider: input.provider ?? existing?.provider ?? null,
|
|
48067
48588
|
metadata: input.metadata ?? {},
|
|
48068
|
-
created_at: timestamp4,
|
|
48589
|
+
created_at: existing?.created_at ?? timestamp4,
|
|
48069
48590
|
updated_at: timestamp4
|
|
48070
48591
|
};
|
|
48071
48592
|
await store.upsert("refs", gitRef, context);
|
|
@@ -48128,7 +48649,7 @@ async function createProject2(input, store, context) {
|
|
|
48128
48649
|
if (!derivedSlug || !taskListId)
|
|
48129
48650
|
throw new Error("Project name and task-list slug must be non-empty");
|
|
48130
48651
|
const project = {
|
|
48131
|
-
id:
|
|
48652
|
+
id: randomUUID4(),
|
|
48132
48653
|
name: input.name,
|
|
48133
48654
|
path: input.path,
|
|
48134
48655
|
description: input.description ?? null,
|
|
@@ -48160,7 +48681,7 @@ async function createPlan2(input, store, context) {
|
|
|
48160
48681
|
store
|
|
48161
48682
|
});
|
|
48162
48683
|
return store.upsert("plans", {
|
|
48163
|
-
id:
|
|
48684
|
+
id: randomUUID4(),
|
|
48164
48685
|
slug,
|
|
48165
48686
|
project_id: projectId,
|
|
48166
48687
|
task_list_id: input.task_list_id ?? context?.taskListId ?? null,
|
|
@@ -48212,7 +48733,7 @@ async function registerAgent2(input, store, context) {
|
|
|
48212
48733
|
}
|
|
48213
48734
|
const timestamp4 = new Date().toISOString();
|
|
48214
48735
|
const agent = {
|
|
48215
|
-
id: existing?.id ??
|
|
48736
|
+
id: existing?.id ?? randomUUID4().slice(0, 8),
|
|
48216
48737
|
name: canonicalName,
|
|
48217
48738
|
description: input.description ?? existing?.description ?? null,
|
|
48218
48739
|
role: input.role ?? existing?.role ?? null,
|
|
@@ -48285,7 +48806,7 @@ async function createTaskList2(input, store, context) {
|
|
|
48285
48806
|
if (!slug)
|
|
48286
48807
|
throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
|
|
48287
48808
|
return store.upsert("task_lists", {
|
|
48288
|
-
id:
|
|
48809
|
+
id: randomUUID4(),
|
|
48289
48810
|
project_id: input.project_id ?? context?.projectId ?? null,
|
|
48290
48811
|
slug,
|
|
48291
48812
|
name: input.name,
|
|
@@ -48320,7 +48841,7 @@ async function updateTaskList2(id, input, store) {
|
|
|
48320
48841
|
async function createTemplate2(input, store, context) {
|
|
48321
48842
|
const timestamp4 = new Date().toISOString();
|
|
48322
48843
|
const template = {
|
|
48323
|
-
id:
|
|
48844
|
+
id: randomUUID4(),
|
|
48324
48845
|
name: input.name,
|
|
48325
48846
|
title_pattern: input.title_pattern,
|
|
48326
48847
|
description: input.description ?? null,
|
|
@@ -48341,7 +48862,7 @@ async function createTemplate2(input, store, context) {
|
|
|
48341
48862
|
}
|
|
48342
48863
|
function buildTemplateTasks(templateId, inputs, timestamp4) {
|
|
48343
48864
|
return inputs.map((input, position) => ({
|
|
48344
|
-
id:
|
|
48865
|
+
id: randomUUID4(),
|
|
48345
48866
|
template_id: templateId,
|
|
48346
48867
|
position,
|
|
48347
48868
|
title_pattern: input.title_pattern,
|
|
@@ -48374,7 +48895,7 @@ async function updateTemplate2(id, input, store) {
|
|
|
48374
48895
|
}
|
|
48375
48896
|
async function logTaskChange2(taskId, action, field, oldValue, newValue, agentId, store, context) {
|
|
48376
48897
|
const entry2 = {
|
|
48377
|
-
id:
|
|
48898
|
+
id: randomUUID4(),
|
|
48378
48899
|
task_id: taskId,
|
|
48379
48900
|
action,
|
|
48380
48901
|
field: field ?? null,
|
|
@@ -48388,7 +48909,7 @@ async function logTaskChange2(taskId, action, field, oldValue, newValue, agentId
|
|
|
48388
48909
|
}
|
|
48389
48910
|
async function addComment2(input, store, context) {
|
|
48390
48911
|
const comment = {
|
|
48391
|
-
id:
|
|
48912
|
+
id: randomUUID4(),
|
|
48392
48913
|
task_id: input.task_id,
|
|
48393
48914
|
agent_id: input.agent_id ?? context?.agentId ?? null,
|
|
48394
48915
|
session_id: input.session_id ?? context?.sessionId ?? null,
|
|
@@ -48430,6 +48951,11 @@ async function importSnapshot(snapshot, store, context) {
|
|
|
48430
48951
|
result.errors.push(...validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists));
|
|
48431
48952
|
if (result.errors.length > 0)
|
|
48432
48953
|
return result;
|
|
48954
|
+
const auditHistory = await preflightAuditHistoryImport2(snapshot.auditHistory, snapshot.tombstones ?? [], store);
|
|
48955
|
+
result.errors.push(...auditHistory.errors);
|
|
48956
|
+
if (result.errors.length > 0)
|
|
48957
|
+
return result;
|
|
48958
|
+
result.skipped += auditHistory.identical;
|
|
48433
48959
|
const entries = [
|
|
48434
48960
|
...snapshot.tasks.map((row) => ["tasks", row]),
|
|
48435
48961
|
...snapshot.projects.map((row) => ["projects", row]),
|
|
@@ -48438,9 +48964,20 @@ async function importSnapshot(snapshot, store, context) {
|
|
|
48438
48964
|
...snapshot.agents.map((row) => ["agents", row]),
|
|
48439
48965
|
...snapshot.taskLists.map((row) => ["task_lists", row]),
|
|
48440
48966
|
...snapshot.templates.map((row) => ["templates", row]),
|
|
48441
|
-
...(snapshot.templateTasks ?? []).map((row) => ["template_tasks", row])
|
|
48442
|
-
...snapshot.auditHistory.map((row) => ["audit_history", row])
|
|
48967
|
+
...(snapshot.templateTasks ?? []).map((row) => ["template_tasks", row])
|
|
48443
48968
|
];
|
|
48969
|
+
for (const row of auditHistory.rowsToInsert) {
|
|
48970
|
+
try {
|
|
48971
|
+
const outcome = await store.insertImmutableAuditHistory(row, context);
|
|
48972
|
+
if (outcome === "inserted")
|
|
48973
|
+
result.inserted += 1;
|
|
48974
|
+
else
|
|
48975
|
+
result.skipped += 1;
|
|
48976
|
+
} catch (error) {
|
|
48977
|
+
result.errors.push(error instanceof Error ? error.message : String(error));
|
|
48978
|
+
return result;
|
|
48979
|
+
}
|
|
48980
|
+
}
|
|
48444
48981
|
for (const [type, row] of entries) {
|
|
48445
48982
|
try {
|
|
48446
48983
|
const existing = await store.get(type, row.id);
|
|
@@ -48474,6 +49011,32 @@ async function importSnapshot(snapshot, store, context) {
|
|
|
48474
49011
|
}
|
|
48475
49012
|
return result;
|
|
48476
49013
|
}
|
|
49014
|
+
async function preflightAuditHistoryImport2(rows, tombstones, store) {
|
|
49015
|
+
const errors2 = tombstones.filter((tombstone) => tombstone.object_type === "audit_history").map((tombstone) => forbiddenAuditHistoryTombstoneError(tombstone.object_id));
|
|
49016
|
+
const rowsToInsert = [];
|
|
49017
|
+
const seen = new Map;
|
|
49018
|
+
let identical = 0;
|
|
49019
|
+
for (const row of rows) {
|
|
49020
|
+
const prior = seen.get(row.id);
|
|
49021
|
+
if (prior) {
|
|
49022
|
+
if (auditHistoryRowsAreFieldIdentical(prior, row))
|
|
49023
|
+
identical += 1;
|
|
49024
|
+
else
|
|
49025
|
+
errors2.push(divergentAuditHistoryReplayError(row.id));
|
|
49026
|
+
continue;
|
|
49027
|
+
}
|
|
49028
|
+
seen.set(row.id, row);
|
|
49029
|
+
const existing = await store.get("audit_history", row.id);
|
|
49030
|
+
if (!existing) {
|
|
49031
|
+
rowsToInsert.push(row);
|
|
49032
|
+
} else if (auditHistoryRowsAreFieldIdentical(existing, row)) {
|
|
49033
|
+
identical += 1;
|
|
49034
|
+
} else {
|
|
49035
|
+
errors2.push(divergentAuditHistoryReplayError(row.id));
|
|
49036
|
+
}
|
|
49037
|
+
}
|
|
49038
|
+
return { rowsToInsert, identical, errors: errors2 };
|
|
49039
|
+
}
|
|
48477
49040
|
async function requireRecord(type, id, store) {
|
|
48478
49041
|
const record = await store.get(type, id);
|
|
48479
49042
|
if (!record)
|
|
@@ -48580,9 +49143,12 @@ var init_postgres_adapter = __esm(() => {
|
|
|
48580
49143
|
init_types();
|
|
48581
49144
|
init_creator_identity();
|
|
48582
49145
|
init_plan_project_link_contract();
|
|
49146
|
+
init_stale_lock_handoff();
|
|
48583
49147
|
init_postgres_sync();
|
|
48584
49148
|
init_integrity();
|
|
48585
49149
|
init_redaction();
|
|
49150
|
+
init_audit_history_import();
|
|
49151
|
+
init_canonical();
|
|
48586
49152
|
TASK_ORDER_BY = `ORDER BY ${TASK_ORDER_TIEBREAK}`;
|
|
48587
49153
|
});
|
|
48588
49154
|
|
|
@@ -50070,25 +50636,25 @@ var init_sqlite = __esm(() => {
|
|
|
50070
50636
|
});
|
|
50071
50637
|
|
|
50072
50638
|
// src/project-registration/authority.ts
|
|
50073
|
-
import { createHash as
|
|
50639
|
+
import { createHash as createHash16 } from "crypto";
|
|
50074
50640
|
function canonicalProjectRegistrationJson(value) {
|
|
50075
|
-
return JSON.stringify(
|
|
50641
|
+
return JSON.stringify(canonicalize3(value));
|
|
50076
50642
|
}
|
|
50077
|
-
function
|
|
50643
|
+
function canonicalize3(value) {
|
|
50078
50644
|
if (Array.isArray(value))
|
|
50079
|
-
return value.map(
|
|
50645
|
+
return value.map(canonicalize3);
|
|
50080
50646
|
if (!value || typeof value !== "object")
|
|
50081
50647
|
return value;
|
|
50082
50648
|
const out = {};
|
|
50083
50649
|
for (const key of Object.keys(value).sort()) {
|
|
50084
50650
|
const entry2 = value[key];
|
|
50085
50651
|
if (entry2 !== undefined)
|
|
50086
|
-
out[key] =
|
|
50652
|
+
out[key] = canonicalize3(entry2);
|
|
50087
50653
|
}
|
|
50088
50654
|
return out;
|
|
50089
50655
|
}
|
|
50090
50656
|
function digestProjectRegistrationValue(value) {
|
|
50091
|
-
return
|
|
50657
|
+
return createHash16("sha256").update(canonicalProjectRegistrationJson(value)).digest("hex");
|
|
50092
50658
|
}
|
|
50093
50659
|
function deriveTodosProjectRegistrationIdempotencyKey(input) {
|
|
50094
50660
|
return `prk_${digestProjectRegistrationValue({
|
|
@@ -51083,31 +51649,6 @@ var init_project_registration = __esm(() => {
|
|
|
51083
51649
|
init_types4();
|
|
51084
51650
|
});
|
|
51085
51651
|
|
|
51086
|
-
// src/task-manifest/canonical.ts
|
|
51087
|
-
import { createHash as createHash16 } from "crypto";
|
|
51088
|
-
function canonicalize3(value) {
|
|
51089
|
-
if (Array.isArray(value))
|
|
51090
|
-
return value.map(canonicalize3);
|
|
51091
|
-
if (value !== null && typeof value === "object") {
|
|
51092
|
-
return Object.fromEntries(Object.entries(value).filter(([, entry2]) => entry2 !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry2]) => [key, canonicalize3(entry2)]));
|
|
51093
|
-
}
|
|
51094
|
-
return value;
|
|
51095
|
-
}
|
|
51096
|
-
function canonicalJson(value) {
|
|
51097
|
-
return JSON.stringify(canonicalize3(value));
|
|
51098
|
-
}
|
|
51099
|
-
function canonicalDigest(value) {
|
|
51100
|
-
return createHash16("sha256").update(canonicalJson(value)).digest("hex");
|
|
51101
|
-
}
|
|
51102
|
-
function deterministicUuid(namespace, ...parts) {
|
|
51103
|
-
const bytes = createHash16("sha256").update([namespace, ...parts].join("\x1F")).digest().subarray(0, 16);
|
|
51104
|
-
bytes[6] = bytes[6] & 15 | 80;
|
|
51105
|
-
bytes[8] = bytes[8] & 63 | 128;
|
|
51106
|
-
const hex = bytes.toString("hex");
|
|
51107
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
51108
|
-
}
|
|
51109
|
-
var init_canonical = () => {};
|
|
51110
|
-
|
|
51111
51652
|
// src/task-manifest/types.ts
|
|
51112
51653
|
var TODOS_TASK_MANIFEST_ROUTE = "todos.task-manifest.v1", TODOS_TASK_MANIFEST_SCHEMA_VERSION = 1, TodosTaskManifestError;
|
|
51113
51654
|
var init_types5 = __esm(() => {
|
|
@@ -53927,6 +54468,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
53927
54468
|
ProjectTaskListEnsureResult: projectTaskListEnsureResultSchema,
|
|
53928
54469
|
ProjectTaskListRollbackResult: projectTaskListRollbackResultSchema,
|
|
53929
54470
|
TaskComment: taskCommentSchema,
|
|
54471
|
+
StaleLockHandoffInput: staleLockHandoffInputSchema,
|
|
54472
|
+
StaleLockHandoffReceipt: staleLockHandoffReceiptSchema,
|
|
53930
54473
|
TaskGitRef: taskGitRefSchema,
|
|
53931
54474
|
Plan: planSchema,
|
|
53932
54475
|
PlanProjectLinkReceipt: planProjectLinkReceiptSchema,
|
|
@@ -53949,6 +54492,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
53949
54492
|
plan_id: { type: "string" },
|
|
53950
54493
|
assigned_to: { type: "string" },
|
|
53951
54494
|
agent_id: { type: "string" },
|
|
54495
|
+
created_by: { type: "string" },
|
|
53952
54496
|
tags: { type: "array", items: { type: "string" } }
|
|
53953
54497
|
}
|
|
53954
54498
|
},
|
|
@@ -55134,6 +55678,51 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
55134
55678
|
}
|
|
55135
55679
|
}
|
|
55136
55680
|
},
|
|
55681
|
+
"/v1/tasks/{id}/stale-lock-handoff": {
|
|
55682
|
+
post: {
|
|
55683
|
+
operationId: "handoffStaleTaskLock",
|
|
55684
|
+
summary: "Atomically transfer one exact stale task lock",
|
|
55685
|
+
description: "Compares one full task UUID, current holder, and exact locked_at version, verifies the lock is strictly older than the supplied threshold, then transfers it directly and writes an immutable task-history receipt in the same backend transaction.",
|
|
55686
|
+
parameters: [
|
|
55687
|
+
{
|
|
55688
|
+
name: "id",
|
|
55689
|
+
in: "path",
|
|
55690
|
+
required: true,
|
|
55691
|
+
schema: { type: "string", format: "uuid" },
|
|
55692
|
+
description: "Exact full task UUID. Short ids and prefixes are rejected."
|
|
55693
|
+
}
|
|
55694
|
+
],
|
|
55695
|
+
requestBody: {
|
|
55696
|
+
required: true,
|
|
55697
|
+
content: {
|
|
55698
|
+
"application/json": {
|
|
55699
|
+
schema: { $ref: "#/components/schemas/StaleLockHandoffInput" }
|
|
55700
|
+
}
|
|
55701
|
+
}
|
|
55702
|
+
},
|
|
55703
|
+
responses: {
|
|
55704
|
+
"200": {
|
|
55705
|
+
content: {
|
|
55706
|
+
"application/json": {
|
|
55707
|
+
schema: {
|
|
55708
|
+
type: "object",
|
|
55709
|
+
additionalProperties: false,
|
|
55710
|
+
required: ["receipt"],
|
|
55711
|
+
properties: {
|
|
55712
|
+
receipt: { $ref: "#/components/schemas/StaleLockHandoffReceipt" }
|
|
55713
|
+
}
|
|
55714
|
+
}
|
|
55715
|
+
}
|
|
55716
|
+
}
|
|
55717
|
+
},
|
|
55718
|
+
"400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
55719
|
+
"403": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
55720
|
+
"404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
55721
|
+
"409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
|
|
55722
|
+
"501": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
|
|
55723
|
+
}
|
|
55724
|
+
}
|
|
55725
|
+
},
|
|
55137
55726
|
"/v1/tasks/{id}/refs": {
|
|
55138
55727
|
get: {
|
|
55139
55728
|
operationId: "listTaskGitRefs",
|
|
@@ -55730,7 +56319,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
55730
56319
|
}
|
|
55731
56320
|
};
|
|
55732
56321
|
}
|
|
55733
|
-
var taskSchema, projectSchema, taskManifestBindingLookupRequestSchema, taskManifestBindingLookupResultSchema, taskManifestBindingLookupResponseSchema, taskListSchema, projectTaskListEnsureReceiptSchema, projectTaskListEnsureResultSchema, projectTaskListRollbackResultSchema, taskCommentSchema, taskGitRefSchema, planSchema, planProjectLinkReceiptSchema, planProjectLinkResultSchema, planProjectLinkRollbackResultSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
|
|
56322
|
+
var taskSchema, projectSchema, taskManifestBindingLookupRequestSchema, taskManifestBindingLookupResultSchema, taskManifestBindingLookupResponseSchema, taskListSchema, projectTaskListEnsureReceiptSchema, projectTaskListEnsureResultSchema, projectTaskListRollbackResultSchema, taskCommentSchema, staleLockHandoffInputSchema, staleLockHandoffReceiptSchema, taskGitRefSchema, planSchema, planProjectLinkReceiptSchema, planProjectLinkResultSchema, planProjectLinkRollbackResultSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
|
|
55734
56323
|
var init_openapi = __esm(() => {
|
|
55735
56324
|
init_package_version();
|
|
55736
56325
|
init_types();
|
|
@@ -55746,9 +56335,12 @@ var init_openapi = __esm(() => {
|
|
|
55746
56335
|
parent_id: { type: "string", nullable: true },
|
|
55747
56336
|
assigned_to: { type: "string", nullable: true },
|
|
55748
56337
|
agent_id: { type: "string", nullable: true },
|
|
56338
|
+
created_by: { type: "string", nullable: true },
|
|
55749
56339
|
reason: { type: "string", nullable: true },
|
|
55750
56340
|
tags: { type: "array", items: { type: "string" } },
|
|
55751
56341
|
version: { type: "number" },
|
|
56342
|
+
locked_by: { type: "string", nullable: true },
|
|
56343
|
+
locked_at: { type: "string", format: "date-time", nullable: true },
|
|
55752
56344
|
created_at: { type: "string" },
|
|
55753
56345
|
updated_at: { type: "string" }
|
|
55754
56346
|
}
|
|
@@ -55913,6 +56505,69 @@ var init_openapi = __esm(() => {
|
|
|
55913
56505
|
created_at: { type: "string", format: "date-time" }
|
|
55914
56506
|
}
|
|
55915
56507
|
};
|
|
56508
|
+
staleLockHandoffInputSchema = {
|
|
56509
|
+
type: "object",
|
|
56510
|
+
additionalProperties: false,
|
|
56511
|
+
required: [
|
|
56512
|
+
"expected_holder",
|
|
56513
|
+
"expected_lock_version",
|
|
56514
|
+
"stale_after_seconds",
|
|
56515
|
+
"new_holder",
|
|
56516
|
+
"reason"
|
|
56517
|
+
],
|
|
56518
|
+
properties: {
|
|
56519
|
+
expected_holder: { type: "string", minLength: 1 },
|
|
56520
|
+
expected_lock_version: {
|
|
56521
|
+
type: "string",
|
|
56522
|
+
format: "date-time",
|
|
56523
|
+
pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$",
|
|
56524
|
+
description: "Exact authoritative locked_at token read from the task; no default or normalization is applied."
|
|
56525
|
+
},
|
|
56526
|
+
stale_after_seconds: {
|
|
56527
|
+
type: "integer",
|
|
56528
|
+
minimum: 1,
|
|
56529
|
+
description: "Lock age threshold supplied by the caller. The lock must be strictly older at the CAS instant."
|
|
56530
|
+
},
|
|
56531
|
+
new_holder: {
|
|
56532
|
+
type: "string",
|
|
56533
|
+
minLength: 1,
|
|
56534
|
+
description: "Must match the agent bound to the authenticated API key."
|
|
56535
|
+
},
|
|
56536
|
+
reason: { type: "string", minLength: 1, maxLength: 4096 }
|
|
56537
|
+
}
|
|
56538
|
+
};
|
|
56539
|
+
staleLockHandoffReceiptSchema = {
|
|
56540
|
+
type: "object",
|
|
56541
|
+
additionalProperties: false,
|
|
56542
|
+
required: [
|
|
56543
|
+
"schema_version",
|
|
56544
|
+
"receipt_id",
|
|
56545
|
+
"task_id",
|
|
56546
|
+
"actor",
|
|
56547
|
+
"previous_holder",
|
|
56548
|
+
"previous_lock_version",
|
|
56549
|
+
"new_holder",
|
|
56550
|
+
"new_lock_version",
|
|
56551
|
+
"stale_after_seconds",
|
|
56552
|
+
"stale_cutoff",
|
|
56553
|
+
"reason",
|
|
56554
|
+
"created_at"
|
|
56555
|
+
],
|
|
56556
|
+
properties: {
|
|
56557
|
+
schema_version: { type: "string", enum: ["todos.stale-lock-handoff.v1"] },
|
|
56558
|
+
receipt_id: { type: "string", format: "uuid" },
|
|
56559
|
+
task_id: { type: "string", format: "uuid" },
|
|
56560
|
+
actor: { type: "string" },
|
|
56561
|
+
previous_holder: { type: "string" },
|
|
56562
|
+
previous_lock_version: { type: "string", format: "date-time" },
|
|
56563
|
+
new_holder: { type: "string" },
|
|
56564
|
+
new_lock_version: { type: "string", format: "date-time" },
|
|
56565
|
+
stale_after_seconds: { type: "integer", minimum: 1 },
|
|
56566
|
+
stale_cutoff: { type: "string", format: "date-time" },
|
|
56567
|
+
reason: { type: "string" },
|
|
56568
|
+
created_at: { type: "string", format: "date-time" }
|
|
56569
|
+
}
|
|
56570
|
+
};
|
|
55916
56571
|
taskGitRefSchema = {
|
|
55917
56572
|
type: "object",
|
|
55918
56573
|
additionalProperties: false,
|
|
@@ -57154,6 +57809,45 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
57154
57809
|
return error(405, `method ${method} not allowed on /v1/tasks`);
|
|
57155
57810
|
}
|
|
57156
57811
|
if (action) {
|
|
57812
|
+
if (action === "stale-lock-handoff") {
|
|
57813
|
+
if (method !== "POST") {
|
|
57814
|
+
return error(405, "method must be POST on /v1/tasks/:id/stale-lock-handoff");
|
|
57815
|
+
}
|
|
57816
|
+
const exactId = normalizeExactTaskId(id);
|
|
57817
|
+
if (!principal.agent) {
|
|
57818
|
+
return error(403, "stale-lock handoff requires an authenticated agent-bound key", {
|
|
57819
|
+
code: "STALE_LOCK_HANDOFF_ACTOR_MISMATCH"
|
|
57820
|
+
});
|
|
57821
|
+
}
|
|
57822
|
+
if (typeof store.tasks.handoffStaleLock !== "function") {
|
|
57823
|
+
return error(501, "stale-lock handoff is not supported by this storage backend");
|
|
57824
|
+
}
|
|
57825
|
+
const body3 = await readJson3(req) ?? {};
|
|
57826
|
+
const allowed = new Set([
|
|
57827
|
+
"expected_holder",
|
|
57828
|
+
"expected_lock_version",
|
|
57829
|
+
"stale_after_seconds",
|
|
57830
|
+
"new_holder",
|
|
57831
|
+
"reason"
|
|
57832
|
+
]);
|
|
57833
|
+
const unknown = Object.keys(body3).find((key2) => !allowed.has(key2));
|
|
57834
|
+
if (unknown) {
|
|
57835
|
+
return error(400, `unknown stale-lock handoff field: ${unknown}`, {
|
|
57836
|
+
code: "STALE_LOCK_HANDOFF_INVALID_INPUT",
|
|
57837
|
+
field: unknown
|
|
57838
|
+
});
|
|
57839
|
+
}
|
|
57840
|
+
const receipt = await store.tasks.handoffStaleLock({
|
|
57841
|
+
task_id: exactId,
|
|
57842
|
+
actor: principal.agent,
|
|
57843
|
+
expected_holder: body3.expected_holder,
|
|
57844
|
+
expected_lock_version: body3.expected_lock_version,
|
|
57845
|
+
stale_after_seconds: body3.stale_after_seconds,
|
|
57846
|
+
new_holder: body3.new_holder,
|
|
57847
|
+
reason: body3.reason
|
|
57848
|
+
}, contextFromPrincipal(principal));
|
|
57849
|
+
return json5({ receipt });
|
|
57850
|
+
}
|
|
57157
57851
|
if (action === "comments") {
|
|
57158
57852
|
if (method === "GET") {
|
|
57159
57853
|
if (!await store.tasks.get(id))
|
|
@@ -57942,7 +58636,24 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
57942
58636
|
if (received === 0) {
|
|
57943
58637
|
return error(400, "empty snapshot: provide at least one record array (tasks/projects/plans/...)");
|
|
57944
58638
|
}
|
|
58639
|
+
const forbiddenAuditTombstone = (snapshot.tombstones ?? []).find((tombstone) => tombstone.object_type === "audit_history");
|
|
58640
|
+
if (forbiddenAuditTombstone) {
|
|
58641
|
+
return error(400, forbiddenAuditHistoryTombstoneError(forbiddenAuditTombstone.object_id), {
|
|
58642
|
+
code: AUDIT_HISTORY_TOMBSTONE_FORBIDDEN,
|
|
58643
|
+
conflict: false,
|
|
58644
|
+
audit_history_id: forbiddenAuditTombstone.object_id
|
|
58645
|
+
});
|
|
58646
|
+
}
|
|
57945
58647
|
const result = await store.sync.importSnapshot(snapshot, contextFromPrincipal(principal));
|
|
58648
|
+
const auditFailureMessage = result.errors.find((message) => parseAuditHistoryImportFailure(message) !== null);
|
|
58649
|
+
if (auditFailureMessage) {
|
|
58650
|
+
const failure = parseAuditHistoryImportFailure(auditFailureMessage);
|
|
58651
|
+
return error(failure.status, auditFailureMessage, {
|
|
58652
|
+
code: failure.code,
|
|
58653
|
+
conflict: failure.conflict,
|
|
58654
|
+
audit_history_id: failure.auditHistoryId
|
|
58655
|
+
});
|
|
58656
|
+
}
|
|
57946
58657
|
return json5({ result, received });
|
|
57947
58658
|
}
|
|
57948
58659
|
return error(404, `unknown /v1 resource: ${resource ?? "(root)"}`);
|
|
@@ -57965,6 +58676,14 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
57965
58676
|
if (e instanceof TaskNotFoundError) {
|
|
57966
58677
|
return error(404, e.message, { code: TaskNotFoundError.code });
|
|
57967
58678
|
}
|
|
58679
|
+
if (e instanceof StaleLockHandoffError) {
|
|
58680
|
+
const status2 = e.code === "STALE_LOCK_HANDOFF_INVALID_TASK_ID" || e.code === "STALE_LOCK_HANDOFF_INVALID_INPUT" ? 400 : e.code === "STALE_LOCK_HANDOFF_ACTOR_MISMATCH" ? 403 : 409;
|
|
58681
|
+
return error(status2, e.message, {
|
|
58682
|
+
code: e.code,
|
|
58683
|
+
conflict: status2 === 409,
|
|
58684
|
+
...e.details
|
|
58685
|
+
});
|
|
58686
|
+
}
|
|
57968
58687
|
if (e instanceof PlanNotFoundError) {
|
|
57969
58688
|
return error(404, e.message, { code: PlanNotFoundError.code });
|
|
57970
58689
|
}
|
|
@@ -57999,6 +58718,8 @@ var init_v1 = __esm(() => {
|
|
|
57999
58718
|
init_redaction();
|
|
58000
58719
|
init_project_task_list_ensure();
|
|
58001
58720
|
init_plan_project_link();
|
|
58721
|
+
init_stale_lock_handoff();
|
|
58722
|
+
init_audit_history_import();
|
|
58002
58723
|
JSON_HEADERS4 = { "Content-Type": "application/json" };
|
|
58003
58724
|
RFC3339_DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})[Tt]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$/;
|
|
58004
58725
|
});
|