@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.
Files changed (58) hide show
  1. package/dist/ai-tools.d.ts +80 -0
  2. package/dist/ai-tools.d.ts.map +1 -0
  3. package/dist/ai.d.ts +313 -0
  4. package/dist/ai.d.ts.map +1 -0
  5. package/dist/cli/cloud-router.d.ts +30 -2
  6. package/dist/cli/cloud-router.d.ts.map +1 -1
  7. package/dist/cli/commands/ai-commands.d.ts +3 -0
  8. package/dist/cli/commands/ai-commands.d.ts.map +1 -0
  9. package/dist/cli/commands/help-commands.d.ts +2 -1
  10. package/dist/cli/commands/help-commands.d.ts.map +1 -1
  11. package/dist/cli/commands/query-commands.d.ts.map +1 -1
  12. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  13. package/dist/cli/index.js +3685 -572
  14. package/dist/cli/stage-a.d.ts +34 -12
  15. package/dist/cli/stage-a.d.ts.map +1 -1
  16. package/dist/contracts.d.ts +1 -0
  17. package/dist/contracts.d.ts.map +1 -1
  18. package/dist/contracts.js +861 -29
  19. package/dist/db/audit.d.ts +8 -0
  20. package/dist/db/audit.d.ts.map +1 -1
  21. package/dist/db/task-lifecycle.d.ts +7 -1
  22. package/dist/db/task-lifecycle.d.ts.map +1 -1
  23. package/dist/db/tasks.d.ts +1 -1
  24. package/dist/db/tasks.d.ts.map +1 -1
  25. package/dist/index.d.ts +3 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +3717 -1205
  28. package/dist/lib/cli-help.d.ts +3 -2
  29. package/dist/lib/cli-help.d.ts.map +1 -1
  30. package/dist/lib/config.d.ts +4 -0
  31. package/dist/lib/config.d.ts.map +1 -1
  32. package/dist/lib/saved-search-views.d.ts.map +1 -1
  33. package/dist/lib/stale-lock-handoff.d.ts +25 -0
  34. package/dist/lib/stale-lock-handoff.d.ts.map +1 -0
  35. package/dist/mcp/index.js +822 -101
  36. package/dist/mcp.js +3 -1
  37. package/dist/project-registration.js +4208 -3709
  38. package/dist/registry.js +826 -29
  39. package/dist/release-provenance.json +5 -5
  40. package/dist/sdk/index.js +8 -1
  41. package/dist/sdk/v1.generated.d.ts +29 -0
  42. package/dist/sdk/v1.generated.d.ts.map +1 -1
  43. package/dist/server/index.js +1155 -434
  44. package/dist/server/openapi.d.ts +189 -0
  45. package/dist/server/openapi.d.ts.map +1 -1
  46. package/dist/server/v1.d.ts.map +1 -1
  47. package/dist/storage/audit-history-import.d.ts +14 -0
  48. package/dist/storage/audit-history-import.d.ts.map +1 -0
  49. package/dist/storage/interfaces.d.ts +12 -1
  50. package/dist/storage/interfaces.d.ts.map +1 -1
  51. package/dist/storage/local-sqlite.d.ts.map +1 -1
  52. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  53. package/dist/storage/sqlite-snapshot.d.ts.map +1 -1
  54. package/dist/storage.js +531 -34
  55. package/dist/task-manifest.js +30 -4
  56. package/dist/types/index.d.ts +43 -0
  57. package/dist/types/index.d.ts.map +1 -1
  58. 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 logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
12183
+ function insertTaskHistory(entry, db) {
12174
12184
  const d = db || getDatabase();
12175
- const id = uuid();
12176
- const timestamp2 = now();
12177
- const machineId = currentStorageMachineId(d);
12178
- const safeOldValue = sanitizeHistoryValue(oldValue, "task_history.old_value");
12179
- const safeNewValue = sanitizeHistoryValue(newValue, "task_history.new_value");
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 (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, taskId, action, field || null, safeOldValue, safeNewValue, agentId || null, timestamp2, machineId]);
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: taskId,
12187
- action,
12188
- field,
12189
- old_value: safeOldValue,
12190
- new_value: safeNewValue,
12191
- actor_id: agentId ?? undefined
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 { id, task_id: taskId, action, field: field || null, old_value: safeOldValue, new_value: safeNewValue, agent_id: agentId || null, created_at: timestamp2, machine_id: machineId };
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 readFileSync2, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync2 } from "fs";
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 = readFileSync2(sourcePath);
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 = readFileSync2(storedPath);
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 = readFileSync2(artifactStorePath(report.relative_path));
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 readFileSync3 } from "fs";
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(readFileSync3(path, "utf8"));
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 created = unwrapTask(await requiredRemoteRoute(client, "/v1/tasks", () => client.create("tasks", input, { retry: false }), ["PARENT_TASK_NOT_FOUND"]));
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
- 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; no success row or local SQLite fallback is permitted");
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.update("tasks", id, patch));
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 readFileSync4, statSync as statSync3 } from "fs";
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(readFileSync4(file, "utf-8"));
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 readFileSync5, statSync as statSync4 } from "fs";
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 = readFileSync5(absolutePath, "utf-8").split(/\r?\n/).length;
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 = readFileSync5(file, "utf-8").split(/\r?\n/);
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 readFileSync6 } from "fs";
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(readFileSync6(join9(root, "package.json"), "utf8"));
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 readFileSync7 } from "fs";
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) ? readFileSync7(input.log_path, "utf-8") : "");
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.19",
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 readFileSync8, writeFileSync as writeFileSync3 } from "fs";
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(readFileSync8(resolve12(path), "utf-8"));
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 readFileSync9, statSync as statSync6 } from "fs";
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(readFileSync9(path, "utf8"));
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 = readFileSync9(manifestPath);
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 readFileSync10, statSync as statSync7 } from "fs";
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 readFileSync10(gitignorePath, "utf-8").split(`
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 = readFileSync10(fullPath, "utf-8");
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 = readFileSync10(fullPath, "utf-8");
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 readFileSync11, statSync as statSync8 } from "fs";
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 = readFileSync11(path);
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
- applyRows("audit_history", "task_history", AUDIT_COLUMNS, snapshot.auditHistory);
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
- return "task_history";
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 randomUUID3 } from "crypto";
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: randomUUID3(),
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: randomUUID3(),
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: randomUUID3(),
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: randomUUID3(),
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: randomUUID3(),
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: randomUUID3(),
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 ?? randomUUID3().slice(0, 8),
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: randomUUID3(),
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: randomUUID3(),
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: randomUUID3(),
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: randomUUID3(),
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: randomUUID3(),
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 createHash15 } from "crypto";
50639
+ import { createHash as createHash16 } from "crypto";
50074
50640
  function canonicalProjectRegistrationJson(value) {
50075
- return JSON.stringify(canonicalize2(value));
50641
+ return JSON.stringify(canonicalize3(value));
50076
50642
  }
50077
- function canonicalize2(value) {
50643
+ function canonicalize3(value) {
50078
50644
  if (Array.isArray(value))
50079
- return value.map(canonicalize2);
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] = canonicalize2(entry2);
50652
+ out[key] = canonicalize3(entry2);
50087
50653
  }
50088
50654
  return out;
50089
50655
  }
50090
50656
  function digestProjectRegistrationValue(value) {
50091
- return createHash15("sha256").update(canonicalProjectRegistrationJson(value)).digest("hex");
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
  });