@hasna/todos 0.15.18 → 0.15.20

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 (47) hide show
  1. package/dist/cli/cloud-router.d.ts +20 -1
  2. package/dist/cli/cloud-router.d.ts.map +1 -1
  3. package/dist/cli/commands/project-commands.d.ts.map +1 -1
  4. package/dist/cli/commands/query-commands.d.ts.map +1 -1
  5. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  6. package/dist/cli/index.js +1306 -122
  7. package/dist/cli/stage-a.d.ts +21 -10
  8. package/dist/cli/stage-a.d.ts.map +1 -1
  9. package/dist/contracts.js +283 -16
  10. package/dist/db/audit.d.ts +8 -0
  11. package/dist/db/audit.d.ts.map +1 -1
  12. package/dist/db/plans.d.ts +4 -0
  13. package/dist/db/plans.d.ts.map +1 -1
  14. package/dist/db/task-lifecycle.d.ts +7 -1
  15. package/dist/db/task-lifecycle.d.ts.map +1 -1
  16. package/dist/db/tasks.d.ts +1 -1
  17. package/dist/db/tasks.d.ts.map +1 -1
  18. package/dist/index.js +599 -26
  19. package/dist/lib/cli-help.d.ts +3 -2
  20. package/dist/lib/cli-help.d.ts.map +1 -1
  21. package/dist/lib/stale-lock-handoff.d.ts +25 -0
  22. package/dist/lib/stale-lock-handoff.d.ts.map +1 -0
  23. package/dist/mcp/index.js +954 -46
  24. package/dist/mcp.js +3 -1
  25. package/dist/project-registration.js +4250 -3684
  26. package/dist/registry.js +283 -16
  27. package/dist/release-provenance.json +5 -5
  28. package/dist/sdk/index.js +7 -0
  29. package/dist/sdk/v1.generated.d.ts +40 -1
  30. package/dist/sdk/v1.generated.d.ts.map +1 -1
  31. package/dist/server/index.js +1287 -379
  32. package/dist/server/openapi.d.ts +232 -0
  33. package/dist/server/openapi.d.ts.map +1 -1
  34. package/dist/server/v1.d.ts.map +1 -1
  35. package/dist/storage/audit-history-import.d.ts +14 -0
  36. package/dist/storage/audit-history-import.d.ts.map +1 -0
  37. package/dist/storage/interfaces.d.ts +22 -1
  38. package/dist/storage/interfaces.d.ts.map +1 -1
  39. package/dist/storage/local-sqlite.d.ts.map +1 -1
  40. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  41. package/dist/storage/shadow.d.ts.map +1 -1
  42. package/dist/storage/sqlite-snapshot.d.ts.map +1 -1
  43. package/dist/storage.js +596 -25
  44. package/dist/task-manifest.js +24 -1
  45. package/dist/types/index.d.ts +50 -0
  46. package/dist/types/index.d.ts.map +1 -1
  47. package/package.json +3 -1
package/dist/cli/index.js CHANGED
@@ -2123,7 +2123,7 @@ var package_default;
2123
2123
  var init_package = __esm(() => {
2124
2124
  package_default = {
2125
2125
  name: "@hasna/todos",
2126
- version: "0.15.18",
2126
+ version: "0.15.20",
2127
2127
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
2128
2128
  type: "module",
2129
2129
  main: "dist/index.js",
@@ -2194,6 +2194,8 @@ var init_package = __esm(() => {
2194
2194
  "dev:mcp": "bun run src/mcp/index.ts",
2195
2195
  "dev:serve": "bun run src/server/index.ts",
2196
2196
  "verify:release": "bun run scripts/verify-public-release.ts --mode=review",
2197
+ "verify:release-review": "bun run scripts/verify-npm-release-agent-review.ts",
2198
+ "issue:release-review": "bun run scripts/issue-npm-release-agent-review.ts",
2197
2199
  prepublishOnly: "bun run scripts/verify-public-release.ts --mode=publish",
2198
2200
  postinstall: "mkdir -p $HOME/.hasna/todos $HOME/.hasna/todos/training 2>/dev/null || true"
2199
2201
  },
@@ -2269,7 +2271,7 @@ function isBlockingDependencyStatus(status) {
2269
2271
  function isTerminalStatus(status) {
2270
2272
  return status === "completed" || status === "failed" || status === "cancelled";
2271
2273
  }
2272
- var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
2274
+ var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanRevisionConflictError, PlanNotFoundError, LockError, StaleLockHandoffError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
2273
2275
  var init_types = __esm(() => {
2274
2276
  TASK_STATUSES = [
2275
2277
  "pending",
@@ -2357,6 +2359,19 @@ var init_types = __esm(() => {
2357
2359
  this.name = "ResourceConflictError";
2358
2360
  }
2359
2361
  };
2362
+ PlanRevisionConflictError = class PlanRevisionConflictError extends Error {
2363
+ planId;
2364
+ expectedUpdatedAt;
2365
+ currentUpdatedAt;
2366
+ static code = "PLAN_REVISION_CONFLICT";
2367
+ constructor(planId, expectedUpdatedAt, currentUpdatedAt) {
2368
+ super(`Plan revision conflict for ${planId}: expected ${expectedUpdatedAt}, current ${currentUpdatedAt}`);
2369
+ this.planId = planId;
2370
+ this.expectedUpdatedAt = expectedUpdatedAt;
2371
+ this.currentUpdatedAt = currentUpdatedAt;
2372
+ this.name = "PlanRevisionConflictError";
2373
+ }
2374
+ };
2360
2375
  PlanNotFoundError = class PlanNotFoundError extends Error {
2361
2376
  planId;
2362
2377
  static code = "PLAN_NOT_FOUND";
@@ -2379,6 +2394,16 @@ var init_types = __esm(() => {
2379
2394
  this.name = "LockError";
2380
2395
  }
2381
2396
  };
2397
+ StaleLockHandoffError = class StaleLockHandoffError extends Error {
2398
+ code;
2399
+ details;
2400
+ constructor(code, message, details = {}) {
2401
+ super(message);
2402
+ this.code = code;
2403
+ this.details = details;
2404
+ this.name = "StaleLockHandoffError";
2405
+ }
2406
+ };
2382
2407
  AgentNotFoundError = class AgentNotFoundError extends Error {
2383
2408
  agentId;
2384
2409
  static code = "AGENT_NOT_FOUND";
@@ -5467,6 +5492,7 @@ var init_http_client = __esm(() => {
5467
5492
  // src/cli/cloud-router.ts
5468
5493
  import { resolveStorageClient } from "@hasna/contracts/client/storage";
5469
5494
  import { normalizeStorageMode } from "@hasna/contracts/mode";
5495
+ import { randomUUID } from "crypto";
5470
5496
  import { resolve as resolvePath } from "path";
5471
5497
  function cleanMode(value) {
5472
5498
  const normalized = value?.trim().toLowerCase();
@@ -5819,6 +5845,45 @@ async function requestRawCloudTaskPage(client, filter) {
5819
5845
  total: parseCloudTaskTotal(res.raw)
5820
5846
  };
5821
5847
  }
5848
+ function cloudPlanTaskListError(planId, detail) {
5849
+ return new Error(`REMOTE_PLAN_TASK_LIST_INCOMPLETE: hosted authority returned an incomplete task set for plan ${planId}; ` + `${detail}; refusing an incomplete plan result`);
5850
+ }
5851
+ function appendExactPlanTaskPage(target, seen, page, planId) {
5852
+ for (const task of page) {
5853
+ if (task.plan_id !== planId) {
5854
+ throw cloudPlanTaskListError(planId, `response includes task ${task.id} from plan ${task.plan_id ?? "null"}`);
5855
+ }
5856
+ if (seen.has(task.id)) {
5857
+ throw cloudPlanTaskListError(planId, `pagination repeats task id ${task.id}`);
5858
+ }
5859
+ seen.add(task.id);
5860
+ target.push(task);
5861
+ }
5862
+ }
5863
+ async function cloudListPlanTasks(client, planId) {
5864
+ const filter = { plan_id: planId, include_subtasks: true };
5865
+ const firstPage = await requestRawCloudTaskPage(client, filter);
5866
+ const tasks = [];
5867
+ const seen = new Set;
5868
+ appendExactPlanTaskPage(tasks, seen, firstPage.tasks, planId);
5869
+ if (firstPage.total === undefined)
5870
+ return tasks;
5871
+ const total = firstPage.total;
5872
+ if (tasks.length > total) {
5873
+ throw cloudPlanTaskListError(planId, `first page contains ${tasks.length} rows but reports total ${total}`);
5874
+ }
5875
+ while (tasks.length < total) {
5876
+ const page = await requestRawCloudTaskPage(client, { ...filter, offset: tasks.length });
5877
+ if (page.total !== total) {
5878
+ throw cloudPlanTaskListError(planId, `pagination total changed from ${total} to ${String(page.total)}`);
5879
+ }
5880
+ if (page.tasks.length === 0 || tasks.length + page.tasks.length > total) {
5881
+ throw cloudPlanTaskListError(planId, "pagination did not make bounded progress toward the reported total");
5882
+ }
5883
+ appendExactPlanTaskPage(tasks, seen, page.tasks, planId);
5884
+ }
5885
+ return tasks;
5886
+ }
5822
5887
  function cloudTaskListFilterError(code, taskListId, detail) {
5823
5888
  return new Error(`${code}: hosted authority returned rows outside requested task_list_id ${taskListId}; ${detail}; refusing an incomplete exact-list result`);
5824
5889
  }
@@ -5935,7 +6000,10 @@ async function cloudGetTask(client, id) {
5935
6000
  }
5936
6001
  async function cloudCreateTask(client, input) {
5937
6002
  const expectedParentId = typeof input["parent_id"] === "string" ? input["parent_id"] : null;
5938
- const created = unwrapTask(await requiredRemoteRoute(client, "/v1/tasks", () => client.create("tasks", input, { retry: false }), ["PARENT_TASK_NOT_FOUND"]));
6003
+ const created = unwrapTask(await requiredRemoteRoute(client, "/v1/tasks", () => client.transport.post("/tasks", input, {
6004
+ idempotencyKey: randomUUID(),
6005
+ retry: false
6006
+ }), ["PARENT_TASK_NOT_FOUND"]));
5939
6007
  if (!created || typeof created.id !== "string" || !created.id.trim()) {
5940
6008
  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");
5941
6009
  }
@@ -5946,7 +6014,7 @@ async function cloudCreateTask(client, input) {
5946
6014
  return persisted;
5947
6015
  }
5948
6016
  async function cloudUpdateTask(client, id, patch) {
5949
- return unwrapTask(await client.update("tasks", id, patch));
6017
+ return unwrapTask(await client.transport.patch(`/tasks/${encodeURIComponent(id)}`, patch));
5950
6018
  }
5951
6019
  async function cloudDeleteTask(client, id) {
5952
6020
  try {
@@ -6004,8 +6072,16 @@ async function cloudTaskAction(client, id, action, body = {}) {
6004
6072
  const raw = await client.transport.post(`/tasks/${encodeURIComponent(id)}/${action}`, body);
6005
6073
  return unwrapTask(raw);
6006
6074
  }
6075
+ function isRemoteTaskIdentity(value) {
6076
+ if (!value || typeof value !== "object" || Array.isArray(value))
6077
+ return false;
6078
+ const id = value["id"];
6079
+ return typeof id === "string" && id.length > 0;
6080
+ }
6007
6081
  async function cloudFailTask(client, id, body = {}) {
6008
6082
  const route = `/v1/tasks/${encodeURIComponent(id)}/fail`;
6083
+ if (body.retry === true)
6084
+ await requireRetryCapability(client);
6009
6085
  const raw = await requiredRemoteRoute(client, route, () => client.transport.post(`/tasks/${encodeURIComponent(id)}/fail`, body));
6010
6086
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
6011
6087
  throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} returned an invalid failure response envelope`);
@@ -6016,7 +6092,7 @@ async function cloudFailTask(client, id, body = {}) {
6016
6092
  }
6017
6093
  const task = result["task"];
6018
6094
  const retryTask = result["retryTask"];
6019
- if (!task || typeof task !== "object" || Array.isArray(task) || retryTask !== undefined && (!retryTask || typeof retryTask !== "object" || Array.isArray(retryTask))) {
6095
+ if (!isRemoteTaskIdentity(task) || body.retry === true && !isRemoteTaskIdentity(retryTask) || body.retry !== true && retryTask !== undefined && !isRemoteTaskIdentity(retryTask)) {
6020
6096
  throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} returned an invalid failure result`);
6021
6097
  }
6022
6098
  return result;
@@ -6039,6 +6115,36 @@ function resolveOpenApiSchema(document, schema) {
6039
6115
  }
6040
6116
  return current && typeof current === "object" && !Array.isArray(current) ? current : null;
6041
6117
  }
6118
+ async function fetchRetryCapability(client) {
6119
+ const document = await requiredRemoteRoute(client, "/v1/openapi.json", () => client.transport.get("/openapi.json"));
6120
+ if (!document || typeof document !== "object" || Array.isArray(document))
6121
+ return false;
6122
+ const doc = document;
6123
+ const paths = doc["paths"];
6124
+ const failPath = paths && typeof paths === "object" && !Array.isArray(paths) ? paths["/v1/tasks/{id}/fail"] : undefined;
6125
+ const post = failPath && typeof failPath === "object" && !Array.isArray(failPath) ? failPath["post"] : undefined;
6126
+ const requestBody = post && typeof post === "object" && !Array.isArray(post) ? post["requestBody"] : undefined;
6127
+ const content = requestBody && typeof requestBody === "object" && !Array.isArray(requestBody) ? requestBody["content"] : undefined;
6128
+ const jsonContent = content && typeof content === "object" && !Array.isArray(content) ? content["application/json"] : undefined;
6129
+ const schema = jsonContent && typeof jsonContent === "object" && !Array.isArray(jsonContent) ? jsonContent["schema"] : undefined;
6130
+ const resolved = resolveOpenApiSchema(document, schema);
6131
+ const properties = resolved?.["properties"];
6132
+ if (!properties || typeof properties !== "object" || Array.isArray(properties))
6133
+ return false;
6134
+ const retry = properties["retry"];
6135
+ return !!retry && typeof retry === "object" && !Array.isArray(retry) && retry["type"] === "boolean";
6136
+ }
6137
+ async function requireRetryCapability(client) {
6138
+ const authority = remoteAuthorityBase(client);
6139
+ let capability = retryCapabilityCache.get(authority);
6140
+ if (!capability) {
6141
+ capability = fetchRetryCapability(client);
6142
+ retryCapabilityCache.set(authority, capability);
6143
+ }
6144
+ if (!await capability) {
6145
+ throw new Error(`REMOTE_RETRY_UNSUPPORTED: configured Todos authority ${authority} does not advertise retry in the ` + "application/json request schema for POST /v1/tasks/{id}/fail; no failure mutation was sent; " + "deploy a compatible @hasna/todos /v1 server before retrying; local SQLite fallback is disabled");
6146
+ }
6147
+ }
6042
6148
  async function fetchCompletionCapabilities(client) {
6043
6149
  const document = await requiredRemoteRoute(client, "/v1/openapi.json", () => client.transport.get("/openapi.json"));
6044
6150
  if (!document || typeof document !== "object" || Array.isArray(document))
@@ -6296,11 +6402,62 @@ function unwrapPlan(raw) {
6296
6402
  }
6297
6403
  return raw;
6298
6404
  }
6405
+ async function cloudGetPlanById(client, id) {
6406
+ const raw = await client.get("plans", id);
6407
+ return raw ? unwrapPlan(raw) : null;
6408
+ }
6409
+ function isUnavailablePlanPatchRoute(error) {
6410
+ if (!error || typeof error !== "object" || error.status !== 404)
6411
+ return false;
6412
+ const body = error.body;
6413
+ const remoteMessage = body && typeof body === "object" && !Array.isArray(body) ? body.error : undefined;
6414
+ return remoteMessage === "unknown /v1 resource: plans";
6415
+ }
6416
+ function isExactPlanCompletionPatch(patch) {
6417
+ const entries = Object.entries(patch).filter(([, value]) => value !== undefined);
6418
+ return entries.length === 1 && entries[0]?.[0] === "status" && entries[0]?.[1] === "completed";
6419
+ }
6299
6420
  async function cloudCreatePlan(client, input) {
6300
6421
  return unwrapPlan(await requiredRemoteRoute(client, "/v1/plans", () => client.create("plans", input)));
6301
6422
  }
6302
6423
  async function cloudUpdatePlan(client, id, patch) {
6303
- return unwrapPlan(await client.update("plans", id, patch));
6424
+ try {
6425
+ return unwrapPlan(await client.update("plans", id, patch));
6426
+ } catch (error) {
6427
+ if (!isExactPlanCompletionPatch(patch) || !isUnavailablePlanPatchRoute(error))
6428
+ throw error;
6429
+ const existing = await cloudGetPlanById(client, id);
6430
+ if (!existing)
6431
+ throw error;
6432
+ if (existing.status === "completed")
6433
+ return existing;
6434
+ const imported = await requiredRemoteRoute(client, "/v1/import", () => client.transport.post("/import", {
6435
+ source: "postgres",
6436
+ planCompletions: [{
6437
+ id,
6438
+ expected_updated_at: existing.updated_at,
6439
+ status: "completed"
6440
+ }]
6441
+ }));
6442
+ const envelope = imported && typeof imported === "object" && !Array.isArray(imported) ? imported : null;
6443
+ const completion = envelope?.planCompletions?.[0];
6444
+ if (envelope?.received !== 1 || !envelope.result || !Array.isArray(envelope.result.errors) || envelope.result.errors.length > 0 || envelope.planCompletions?.length !== 1 || completion?.id !== id || completion.status !== "completed" || completion.expected_updated_at !== existing.updated_at || typeof completion.result_updated_at !== "string" || typeof completion.applied !== "boolean") {
6445
+ throw new Error("REMOTE_API_INCOMPATIBLE: /v1/import did not confirm one atomic plan completion; " + "local SQLite fallback is disabled");
6446
+ }
6447
+ const persisted = await cloudGetPlanById(client, id);
6448
+ if (!persisted) {
6449
+ throw new Error(`REMOTE_API_INCOMPATIBLE: /v1/import acknowledged plan completion ${id} but authoritative readback returned no plan; ` + "local SQLite fallback is disabled");
6450
+ }
6451
+ if (persisted.status !== "completed" || persisted.updated_at !== completion.result_updated_at) {
6452
+ throw new Error(`REMOTE_API_INCOMPATIBLE: /v1/import completion readback did not match its receipt for plan ${id}; ` + "local SQLite fallback is disabled");
6453
+ }
6454
+ for (const key of PLAN_COMPLETION_PROTECTED_FIELDS) {
6455
+ if (persisted[key] !== existing[key]) {
6456
+ throw new Error(`REMOTE_PLAN_COMPLETION_CONFLICT: /v1/import completion changed protected plan field ${key}; ` + "local SQLite fallback is disabled");
6457
+ }
6458
+ }
6459
+ return persisted;
6460
+ }
6304
6461
  }
6305
6462
  async function cloudDeletePlan(client, id) {
6306
6463
  try {
@@ -6608,6 +6765,19 @@ async function cloudUnlockTask(client, id, agentId, force = false) {
6608
6765
  }
6609
6766
  return true;
6610
6767
  }
6768
+ async function cloudHandoffStaleTaskLock(client, input) {
6769
+ const raw = await client.transport.post(`/tasks/${encodeURIComponent(input.task_id)}/stale-lock-handoff`, {
6770
+ expected_holder: input.expected_holder,
6771
+ expected_lock_version: input.expected_lock_version,
6772
+ stale_after_seconds: input.stale_after_seconds,
6773
+ new_holder: input.new_holder,
6774
+ reason: input.reason
6775
+ });
6776
+ if (raw && typeof raw === "object" && "receipt" in raw) {
6777
+ return raw.receipt;
6778
+ }
6779
+ throw new Error("STALE_LOCK_HANDOFF_RECEIPT_MISSING: remote response did not include receipt");
6780
+ }
6611
6781
  async function cloudGetDependencies(client, id) {
6612
6782
  const raw = await client.transport.get(`/tasks/${encodeURIComponent(id)}/dependencies`);
6613
6783
  const env = raw ?? {};
@@ -7043,7 +7213,7 @@ async function cloudTimeline(client, options = {}) {
7043
7213
  const limit = options.limit ?? 50;
7044
7214
  return { entries: entries.slice(offset, offset + limit), total, limit, offset };
7045
7215
  }
7046
- var UUID_RE, TRANSPORT_TOKENS, COMPLETION_EVIDENCE_FIELDS, completionCapabilityCache, gitRefCapabilityCache, SERVER_MODE_CANDIDATES, cachedServerMode = null, PRIORITY_RANK, listTagsCapabilityCache, RELATION_HYDRATION_CONCURRENCY = 6;
7216
+ var UUID_RE, TRANSPORT_TOKENS, COMPLETION_EVIDENCE_FIELDS, completionCapabilityCache, retryCapabilityCache, gitRefCapabilityCache, SERVER_MODE_CANDIDATES, cachedServerMode = null, PRIORITY_RANK, listTagsCapabilityCache, PLAN_COMPLETION_PROTECTED_FIELDS, RELATION_HYDRATION_CONCURRENCY = 6;
7047
7217
  var init_cloud_router = __esm(() => {
7048
7218
  init_types();
7049
7219
  init_redaction();
@@ -7068,13 +7238,32 @@ var init_cloud_router = __esm(() => {
7068
7238
  "confidence"
7069
7239
  ];
7070
7240
  completionCapabilityCache = new Map;
7241
+ retryCapabilityCache = new Map;
7071
7242
  gitRefCapabilityCache = new Map;
7072
7243
  SERVER_MODE_CANDIDATES = ["postgres", "cloud", "self_hosted"];
7073
7244
  PRIORITY_RANK = { critical: 0, high: 1, medium: 2, low: 3 };
7074
7245
  listTagsCapabilityCache = new Map;
7246
+ PLAN_COMPLETION_PROTECTED_FIELDS = [
7247
+ "id",
7248
+ "slug",
7249
+ "project_id",
7250
+ "task_list_id",
7251
+ "agent_id",
7252
+ "name",
7253
+ "description",
7254
+ "created_at",
7255
+ "machine_id",
7256
+ "synced_at"
7257
+ ];
7075
7258
  });
7076
7259
 
7077
7260
  // src/cli/stage-a.ts
7261
+ function applyTodosCliAuthorityEnvironment(authority, env = process.env) {
7262
+ if (authority.route !== "local" || authority.selected_by !== "local-only-command")
7263
+ return;
7264
+ env.HASNA_TODOS_STORAGE_MODE = "sqlite";
7265
+ env.TODOS_STORAGE_MODE = "sqlite";
7266
+ }
7078
7267
  function isTodosCliCommandVisibleForRoute(command, route) {
7079
7268
  if (route === "local")
7080
7269
  return true;
@@ -7284,7 +7473,7 @@ function nearestCommands(command, limit = 3) {
7284
7473
  const threshold = command.length <= 4 ? 1 : command.length <= 8 ? 2 : 3;
7285
7474
  return [...COMMAND_CAPABILITY_MATRIX.entries()].filter(([, owner]) => owner !== "local-only").map(([candidate]) => candidate).map((candidate) => ({ candidate, distance: editDistance(command, candidate) })).filter(({ distance }) => distance <= threshold).sort((left, right) => left.distance - right.distance || left.candidate.localeCompare(right.candidate)).slice(0, limit).map(({ candidate }) => candidate);
7286
7475
  }
7287
- function assertRemoteCommandSupported(invocation) {
7476
+ function assertInvocationRoutable(invocation) {
7288
7477
  if (invocation.invalidGlobalOption) {
7289
7478
  throw new Error(`REMOTE_COMMAND_UNSUPPORTED: the global option ${invocation.invalidGlobalOption} was given without a value; ` + `pass one as \`${invocation.invalidGlobalOption} <value>\``);
7290
7479
  }
@@ -7298,8 +7487,12 @@ function assertRemoteCommandSupported(invocation) {
7298
7487
  const didYouMean = suggestions.length > 0 ? ` Did you mean: ${suggestions.join(", ")}?` : "";
7299
7488
  throw new Error(`UNKNOWN_COMMAND: \`${command}\` is not a built-in todos command on the /v1 route.${didYouMean} ` + "Run `todos --help` for the commands available here; verbs contributed by optional packages are local-only. " + "(This is not a connectivity, storage-mode or credential problem.)");
7300
7489
  }
7490
+ return owner;
7491
+ }
7492
+ function assertRemoteCommandSupported(invocation, owner) {
7493
+ const command = invocation.command;
7301
7494
  if (command && owner === "local-only") {
7302
- throw new Error(`REMOTE_COMMAND_UNSUPPORTED: \`${command}\` is a local-only command and the Todos /v1 authority does not ` + "serve it; local SQLite fallback is disabled. Run `todos --help` to see the commands this route supports.");
7495
+ throw new Error(`LOCAL_COMMAND_ROUTING_INVARIANT: \`${command}\` must select the local command route before remote authority validation`);
7303
7496
  }
7304
7497
  if (!command || !commandSupportsRemote(invocation)) {
7305
7498
  const blame = command ? disqualifyingArgument(invocation) : null;
@@ -7317,7 +7510,11 @@ function initializeTodosCliAuthority(args = process.argv.slice(2), env = process
7317
7510
  const status = getTodosRemoteAuthorityConfigStatus(env);
7318
7511
  return { route: "remote-diagnostic", v1_base_url: status.v1_base_url };
7319
7512
  }
7320
- assertRemoteCommandSupported(invocation);
7513
+ const owner = assertInvocationRoutable(invocation);
7514
+ if (owner === "local-only") {
7515
+ return { route: "local", v1_base_url: null, selected_by: "local-only-command" };
7516
+ }
7517
+ assertRemoteCommandSupported(invocation, owner);
7321
7518
  const client = getTodosCloudClient(env);
7322
7519
  if (!client) {
7323
7520
  throw new Error("REMOTE_API_UNAVAILABLE: remote mode did not resolve an HTTP client; local SQLite fallback is disabled");
@@ -7451,6 +7648,7 @@ var init_stage_a = __esm(() => {
7451
7648
  "sprint",
7452
7649
  "stale",
7453
7650
  "standup",
7651
+ "stale-lock-handoff",
7454
7652
  "start",
7455
7653
  "status",
7456
7654
  "steal",
@@ -7583,6 +7781,7 @@ var init_stage_a = __esm(() => {
7583
7781
  "tag",
7584
7782
  "task",
7585
7783
  "task-lists",
7784
+ "stale-lock-handoff",
7586
7785
  "template-export",
7587
7786
  "template-import",
7588
7787
  "template-preview",
@@ -7590,6 +7789,7 @@ var init_stage_a = __esm(() => {
7590
7789
  "timeline",
7591
7790
  "tl",
7592
7791
  "unlock",
7792
+ "unassign",
7593
7793
  "untag",
7594
7794
  "update"
7595
7795
  ]);
@@ -13475,7 +13675,7 @@ __export(exports_event_hooks, {
13475
13675
  emitLocalEventHooks: () => emitLocalEventHooks,
13476
13676
  LOCAL_EVENT_TYPES: () => LOCAL_EVENT_TYPES
13477
13677
  });
13478
- import { createHash as createHash4, randomUUID } from "crypto";
13678
+ import { createHash as createHash4, randomUUID as randomUUID2 } from "crypto";
13479
13679
  import { appendFileSync, mkdirSync as mkdirSync4 } from "fs";
13480
13680
  import { dirname as dirname4, resolve as resolve7 } from "path";
13481
13681
  import { createConnection } from "net";
@@ -13536,7 +13736,7 @@ function canonicalEvent(input) {
13536
13736
  }
13537
13737
  function buildEnvelope(type, payload, timestamp2 = new Date().toISOString()) {
13538
13738
  const base = {
13539
- id: randomUUID(),
13739
+ id: randomUUID2(),
13540
13740
  type,
13541
13741
  timestamp: timestamp2,
13542
13742
  payload: redactValue(payload ?? {}),
@@ -13739,7 +13939,7 @@ import { existsSync as existsSync6 } from "fs";
13739
13939
  import { homedir as homedir2 } from "os";
13740
13940
  import { join as join5 } from "path";
13741
13941
  import { createHmac, timingSafeEqual } from "crypto";
13742
- import { randomUUID as randomUUID2 } from "crypto";
13942
+ import { randomUUID as randomUUID3 } from "crypto";
13743
13943
  import { spawn } from "child_process";
13744
13944
  import { randomUUID as randomUUID22 } from "crypto";
13745
13945
  function getPathValue(input, path) {
@@ -14097,7 +14297,7 @@ async function dispatchChannel(event, channel, options = {}) {
14097
14297
  function createDeliveryResult(event, channel, attempts) {
14098
14298
  const status = attempts.some((attempt) => attempt.status === "success") ? "success" : attempts.every((attempt) => attempt.status === "skipped") ? "skipped" : "failed";
14099
14299
  return {
14100
- id: randomUUID2(),
14300
+ id: randomUUID3(),
14101
14301
  eventId: event.id,
14102
14302
  channelId: channel.id,
14103
14303
  transport: channel.transport,
@@ -15099,6 +15299,7 @@ var init_activity_audit = __esm(() => {
15099
15299
  var exports_audit = {};
15100
15300
  __export(exports_audit, {
15101
15301
  logTaskChange: () => logTaskChange,
15302
+ insertTaskHistory: () => insertTaskHistory,
15102
15303
  getTaskHistory: () => getTaskHistory,
15103
15304
  getRecentActivity: () => getRecentActivity,
15104
15305
  getRecap: () => getRecap
@@ -15106,28 +15307,55 @@ __export(exports_audit, {
15106
15307
  function sanitizeHistoryValue(value, context) {
15107
15308
  return value === undefined || value === null ? null : sanitizePreWriteText(String(value), context);
15108
15309
  }
15109
- function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
15310
+ function insertTaskHistory(entry, db) {
15110
15311
  const d = db || getDatabase();
15111
- const id = uuid();
15112
- const timestamp2 = now();
15113
- const machineId = currentStorageMachineId(d);
15114
- const safeOldValue = sanitizeHistoryValue(oldValue, "task_history.old_value");
15115
- const safeNewValue = sanitizeHistoryValue(newValue, "task_history.new_value");
15312
+ const safeEntry = {
15313
+ ...entry,
15314
+ field: entry.field || null,
15315
+ old_value: sanitizeHistoryValue(entry.old_value, "task_history.old_value"),
15316
+ new_value: sanitizeHistoryValue(entry.new_value, "task_history.new_value"),
15317
+ agent_id: entry.agent_id || null,
15318
+ machine_id: entry.machine_id ?? currentStorageMachineId(d)
15319
+ };
15116
15320
  d.run(`INSERT INTO task_history (id, task_id, action, field, old_value, new_value, agent_id, created_at, machine_id)
15117
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [id, taskId, action, field || null, safeOldValue, safeNewValue, agentId || null, timestamp2, machineId]);
15321
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
15322
+ safeEntry.id,
15323
+ safeEntry.task_id,
15324
+ safeEntry.action,
15325
+ safeEntry.field,
15326
+ safeEntry.old_value,
15327
+ safeEntry.new_value,
15328
+ safeEntry.agent_id,
15329
+ safeEntry.created_at,
15330
+ safeEntry.machine_id ?? null
15331
+ ]);
15118
15332
  try {
15119
15333
  const { logActivity: logActivity2 } = (init_activity_audit(), __toCommonJS(exports_activity_audit));
15120
15334
  logActivity2({
15121
15335
  entity_type: "task",
15122
- entity_id: taskId,
15123
- action,
15124
- field,
15125
- old_value: safeOldValue,
15126
- new_value: safeNewValue,
15127
- actor_id: agentId ?? undefined
15336
+ entity_id: safeEntry.task_id,
15337
+ action: safeEntry.action,
15338
+ field: safeEntry.field ?? undefined,
15339
+ old_value: safeEntry.old_value,
15340
+ new_value: safeEntry.new_value,
15341
+ actor_id: safeEntry.agent_id ?? undefined
15128
15342
  }, d);
15129
15343
  } catch {}
15130
- 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 };
15344
+ return safeEntry;
15345
+ }
15346
+ function logTaskChange(taskId, action, field, oldValue, newValue, agentId, db) {
15347
+ const d = db || getDatabase();
15348
+ return insertTaskHistory({
15349
+ id: uuid(),
15350
+ task_id: taskId,
15351
+ action,
15352
+ field: field || null,
15353
+ old_value: oldValue ?? null,
15354
+ new_value: newValue ?? null,
15355
+ agent_id: agentId || null,
15356
+ created_at: now(),
15357
+ machine_id: currentStorageMachineId(d)
15358
+ }, d);
15131
15359
  }
15132
15360
  function getTaskHistory(taskId, db) {
15133
15361
  const d = db || getDatabase();
@@ -16188,6 +16416,145 @@ var init_task_graph = __esm(() => {
16188
16416
  init_task_crud();
16189
16417
  });
16190
16418
 
16419
+ // src/lib/stale-lock-handoff.ts
16420
+ function normalizeExactTaskId(value) {
16421
+ if (typeof value !== "string" || !EXACT_TASK_UUID_RE.test(value.trim())) {
16422
+ 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 });
16423
+ }
16424
+ return value.trim().toLowerCase();
16425
+ }
16426
+ function requireNonEmptyString(value, field) {
16427
+ if (typeof value !== "string" || !value.trim()) {
16428
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", `${field} must be a non-empty string`, { field });
16429
+ }
16430
+ const trimmed = value.trim();
16431
+ if (field === "reason" && trimmed.length > MAX_REASON_LENGTH) {
16432
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", `reason must be at most ${MAX_REASON_LENGTH} characters`, { field, max_length: MAX_REASON_LENGTH });
16433
+ }
16434
+ return trimmed;
16435
+ }
16436
+ function requireCanonicalLockVersion(value) {
16437
+ if (typeof value !== "string" || !CANONICAL_LOCK_VERSION_RE.test(value)) {
16438
+ 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" });
16439
+ }
16440
+ const parsed = Date.parse(value);
16441
+ if (Number.isNaN(parsed) || new Date(parsed).toISOString() !== value) {
16442
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "expected_lock_version must name a real canonical UTC instant", { field: "expected_lock_version" });
16443
+ }
16444
+ return value;
16445
+ }
16446
+ function requireStaleThreshold(value) {
16447
+ if (!Number.isSafeInteger(value) || Number(value) <= 0) {
16448
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "stale_after_seconds must be a positive safe integer", { field: "stale_after_seconds" });
16449
+ }
16450
+ return Number(value);
16451
+ }
16452
+ function prepareStaleLockHandoff(input, options = {}) {
16453
+ const taskId = normalizeExactTaskId(input.task_id);
16454
+ const actor = requireNonEmptyString(input.actor, "actor");
16455
+ const expectedHolder = requireNonEmptyString(input.expected_holder, "expected_holder");
16456
+ const newHolder = requireNonEmptyString(input.new_holder, "new_holder");
16457
+ const expectedLockVersion = requireCanonicalLockVersion(input.expected_lock_version);
16458
+ const staleAfterSeconds = requireStaleThreshold(input.stale_after_seconds);
16459
+ const reason = sanitizePreWriteText(requireNonEmptyString(input.reason, "reason"), "stale_lock_handoff.reason").trim();
16460
+ if (!reason) {
16461
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "reason must remain non-empty after safety filtering", { field: "reason" });
16462
+ }
16463
+ if (canonicalAgentRef(actor) !== canonicalAgentRef(newHolder)) {
16464
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_ACTOR_MISMATCH", "new_holder must match the authenticated actor", { actor, new_holder: newHolder });
16465
+ }
16466
+ if (canonicalAgentRef(expectedHolder) === canonicalAgentRef(newHolder)) {
16467
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "new_holder must differ from expected_holder", { field: "new_holder" });
16468
+ }
16469
+ const operationTimestamp = options.now ?? new Date().toISOString();
16470
+ if (!CANONICAL_LOCK_VERSION_RE.test(operationTimestamp) || new Date(Date.parse(operationTimestamp)).toISOString() !== operationTimestamp) {
16471
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_INVALID_INPUT", "operation timestamp must be a canonical UTC instant");
16472
+ }
16473
+ const staleCutoff = new Date(Date.parse(operationTimestamp) - staleAfterSeconds * 1000).toISOString();
16474
+ return {
16475
+ task_id: taskId,
16476
+ actor,
16477
+ expected_holder: expectedHolder,
16478
+ expected_lock_version: expectedLockVersion,
16479
+ stale_after_seconds: staleAfterSeconds,
16480
+ new_holder: newHolder,
16481
+ reason,
16482
+ operation_timestamp: operationTimestamp,
16483
+ stale_cutoff: staleCutoff,
16484
+ receipt_id: options.receiptId ?? crypto.randomUUID()
16485
+ };
16486
+ }
16487
+ function buildStaleLockHandoffReceipt(input) {
16488
+ return {
16489
+ schema_version: STALE_LOCK_HANDOFF_SCHEMA_VERSION,
16490
+ receipt_id: input.receipt_id,
16491
+ task_id: input.task_id,
16492
+ actor: input.actor,
16493
+ previous_holder: input.expected_holder,
16494
+ previous_lock_version: input.expected_lock_version,
16495
+ new_holder: input.new_holder,
16496
+ new_lock_version: input.operation_timestamp,
16497
+ stale_after_seconds: input.stale_after_seconds,
16498
+ stale_cutoff: input.stale_cutoff,
16499
+ reason: input.reason,
16500
+ created_at: input.operation_timestamp
16501
+ };
16502
+ }
16503
+ function staleLockHandoffHistory(receipt, machineId) {
16504
+ return {
16505
+ id: receipt.receipt_id,
16506
+ task_id: receipt.task_id,
16507
+ action: STALE_LOCK_HANDOFF_ACTION,
16508
+ field: STALE_LOCK_HANDOFF_FIELD,
16509
+ old_value: JSON.stringify({
16510
+ holder: receipt.previous_holder,
16511
+ lock_version: receipt.previous_lock_version
16512
+ }),
16513
+ new_value: JSON.stringify(receipt),
16514
+ agent_id: receipt.actor,
16515
+ created_at: receipt.created_at,
16516
+ machine_id: machineId
16517
+ };
16518
+ }
16519
+ function throwStaleLockHandoffConflict(task, input) {
16520
+ if (!task.locked_by || !task.locked_at) {
16521
+ 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 });
16522
+ }
16523
+ if (task.locked_at !== input.expected_lock_version) {
16524
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_VERSION_MISMATCH", `Task ${input.task_id} lock version changed`, {
16525
+ task_id: input.task_id,
16526
+ expected_lock_version: input.expected_lock_version,
16527
+ current_lock_version: task.locked_at
16528
+ });
16529
+ }
16530
+ if (task.locked_by !== input.expected_holder) {
16531
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_HOLDER_MISMATCH", `Task ${input.task_id} lock holder changed`, {
16532
+ task_id: input.task_id,
16533
+ expected_holder: input.expected_holder,
16534
+ current_holder: task.locked_by
16535
+ });
16536
+ }
16537
+ if (isTerminalStatus(task.status)) {
16538
+ 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 });
16539
+ }
16540
+ if (task.locked_at >= input.stale_cutoff) {
16541
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_NOT_STALE", `Task ${input.task_id} lock is not older than the supplied stale threshold`, {
16542
+ task_id: input.task_id,
16543
+ current_lock_version: task.locked_at,
16544
+ stale_cutoff: input.stale_cutoff
16545
+ });
16546
+ }
16547
+ throw new StaleLockHandoffError("STALE_LOCK_HANDOFF_CONFLICT", `Task ${input.task_id} changed during stale-lock handoff`, { task_id: input.task_id });
16548
+ }
16549
+ 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;
16550
+ var init_stale_lock_handoff = __esm(() => {
16551
+ init_types();
16552
+ init_creator_identity();
16553
+ init_prewrite_secrets();
16554
+ 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;
16555
+ CANONICAL_LOCK_VERSION_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
16556
+ });
16557
+
16191
16558
  // src/db/task-lifecycle.ts
16192
16559
  var exports_task_lifecycle = {};
16193
16560
  __export(exports_task_lifecycle, {
@@ -16196,6 +16563,7 @@ __export(exports_task_lifecycle, {
16196
16563
  startTask: () => startTask,
16197
16564
  spawnNextRecurrence: () => spawnNextRecurrence,
16198
16565
  lockTask: () => lockTask,
16566
+ handoffStaleTaskLock: () => handoffStaleTaskLock,
16199
16567
  getTasksChangedSince: () => getTasksChangedSince,
16200
16568
  getTaskLockStatus: () => getTaskLockStatus,
16201
16569
  getStaleTasks: () => getStaleTasks,
@@ -16464,6 +16832,38 @@ function unlockTask(id, agentId, db) {
16464
16832
  WHERE id = ?`, [timestamp2, id]);
16465
16833
  return true;
16466
16834
  }
16835
+ function handoffStaleTaskLock(input, db) {
16836
+ const d = db || getDatabase();
16837
+ const prepared = prepareStaleLockHandoff(input);
16838
+ const receipt = buildStaleLockHandoffReceipt(prepared);
16839
+ const history = staleLockHandoffHistory(receipt, null);
16840
+ const transfer = d.transaction(() => {
16841
+ const result = d.run(`UPDATE tasks
16842
+ SET locked_by = ?, locked_at = ?, updated_at = ?, version = version + 1
16843
+ WHERE id = ?
16844
+ AND locked_by = ?
16845
+ AND locked_at = ?
16846
+ AND julianday(locked_at) < julianday(?)
16847
+ AND status NOT IN ('completed', 'failed', 'cancelled')`, [
16848
+ prepared.new_holder,
16849
+ prepared.operation_timestamp,
16850
+ prepared.operation_timestamp,
16851
+ prepared.task_id,
16852
+ prepared.expected_holder,
16853
+ prepared.expected_lock_version,
16854
+ prepared.stale_cutoff
16855
+ ]);
16856
+ if (result.changes === 0) {
16857
+ const current = getTask(prepared.task_id, d);
16858
+ if (!current)
16859
+ throw new TaskNotFoundError(prepared.task_id);
16860
+ throwStaleLockHandoffConflict(current, prepared);
16861
+ }
16862
+ insertTaskHistory(history, d);
16863
+ });
16864
+ transfer();
16865
+ return receipt;
16866
+ }
16467
16867
  function getTaskLockStatus(id, db) {
16468
16868
  const d = db || getDatabase();
16469
16869
  const task = getTask(id, d);
@@ -16764,6 +17164,7 @@ var init_task_lifecycle = __esm(() => {
16764
17164
  init_task_crud();
16765
17165
  init_task_graph();
16766
17166
  init_prewrite_secrets();
17167
+ init_stale_lock_handoff();
16767
17168
  });
16768
17169
 
16769
17170
  // src/db/task-crud.ts
@@ -18333,6 +18734,48 @@ function updatePlan(id, input, db) {
18333
18734
  return updatePlanStored(id, input, d);
18334
18735
  })();
18335
18736
  }
18737
+ function nextPlanCompletionTimestamp(expectedUpdatedAt) {
18738
+ const expected = Date.parse(expectedUpdatedAt);
18739
+ const minimum = Number.isNaN(expected) ? Date.now() : expected + 2;
18740
+ return new Date(Math.max(Date.now(), minimum)).toISOString();
18741
+ }
18742
+ function completePlanAtRevision(id, expectedUpdatedAt, db) {
18743
+ const d = db || getDatabase();
18744
+ return d.transaction(() => {
18745
+ guardPlanRowsSqlite([id], d);
18746
+ const plan = getPlan(id, d);
18747
+ if (!plan)
18748
+ throw new PlanNotFoundError(id);
18749
+ if (plan.updated_at !== expectedUpdatedAt) {
18750
+ throw new PlanRevisionConflictError(id, expectedUpdatedAt, plan.updated_at);
18751
+ }
18752
+ if (plan.status === "completed")
18753
+ return { plan, applied: false };
18754
+ const updatedAt = nextPlanCompletionTimestamp(expectedUpdatedAt);
18755
+ const result = d.run(`UPDATE plans
18756
+ SET status = 'completed', updated_at = ?
18757
+ WHERE id = ? AND updated_at = ? AND status <> 'completed'`, [updatedAt, id, expectedUpdatedAt]);
18758
+ if (result.changes !== 1) {
18759
+ const current = getPlan(id, d);
18760
+ if (!current)
18761
+ throw new PlanNotFoundError(id);
18762
+ throw new PlanRevisionConflictError(id, expectedUpdatedAt, current.updated_at);
18763
+ }
18764
+ const completed = getPlan(id, d);
18765
+ emitLocalEventHooksQuiet({
18766
+ type: "plan.updated",
18767
+ payload: {
18768
+ id,
18769
+ old_status: plan.status,
18770
+ new_status: completed.status,
18771
+ name: completed.name,
18772
+ project_id: completed.project_id
18773
+ },
18774
+ databasePath: databasePathFromDatabase(d)
18775
+ });
18776
+ return { plan: completed, applied: true };
18777
+ })();
18778
+ }
18336
18779
  function deletePlan(id, db) {
18337
18780
  const d = db || getDatabase();
18338
18781
  const plan = getPlan(id, d);
@@ -20331,6 +20774,7 @@ __export(exports_tasks, {
20331
20774
  insertTaskTags: () => insertTaskTags,
20332
20775
  importTaskBoardBundle: () => importTaskBoardBundle,
20333
20776
  importCalendarIcs: () => importCalendarIcs,
20777
+ handoffStaleTaskLock: () => handoffStaleTaskLock,
20334
20778
  getTimeReport: () => getTimeReport,
20335
20779
  getTimeLogs: () => getTimeLogs,
20336
20780
  getTasksChangedSince: () => getTasksChangedSince,
@@ -21973,7 +22417,7 @@ function registerTaskCommands(program2) {
21973
22417
  }
21974
22418
  });
21975
22419
  const task = program2.command("task").description("Task subcommands for deterministic automation");
21976
- task.command("upsert").description("Create or update a task by stable metadata fingerprint").requiredOption("--fingerprint <key>", "Stable dedupe fingerprint").requiredOption("--title <text>", "Task title").option("-d, --description <text>", "Task description").option("-p, --priority <level>", "Priority: low, medium, high, critical").option("-s, --status <status>", "Task status").option("--list <id>", "Task list ID").option("--task-list <id>", "Task list ID (alias for --list)").option("-t, --tags <tags>", "Comma-separated tags").option("--tag <tags>", "Comma-separated tags (alias for --tags)").option("--metadata-json <json>", "JSON object merged into task metadata").option("--working-dir <path>", "Working directory to store on create/update").option("--project <id>", "Assign to project by ID, slug, or path").option("--assign <agent>", "Assign to agent").option("--assign-seat", "Allow --assign to name a durable seat (a seat queue has no session watching it)").option("--expectation-id <id>", "Expectation metadata ID").option("--expectation-fingerprint <key>", "Expectation metadata fingerprint").option("--evidence-paths <paths>", "Comma-separated evidence paths").option("--origin-loop-id <id>", "Origin loop ID").option("--origin-run-id <id>", "Origin run ID").option("--expected <json-or-text>", "Expected value metadata").option("--observed <json-or-text>", "Observed value metadata").option("--acceptance <json-or-text>", "Acceptance metadata").action(async (opts) => {
22420
+ task.command("upsert").description("Create or update a task by stable metadata fingerprint").requiredOption("--fingerprint <key>", "Stable dedupe fingerprint").requiredOption("--title <text>", "Task title").option("-d, --description <text>", "Task description").option("-p, --priority <level>", "Priority: low, medium, high, critical").option("-s, --status <status>", "Task status").option("--list <id>", "Task list ID").option("--task-list <id>", "Task list ID (alias for --list)").option("--plan <id>", "Assign to a plan").option("-t, --tags <tags>", "Comma-separated tags").option("--tag <tags>", "Comma-separated tags (alias for --tags)").option("--metadata-json <json>", "JSON object merged into task metadata").option("--working-dir <path>", "Working directory to store on create/update").option("--project <id>", "Assign to project by ID, slug, or path").option("--assign <agent>", "Assign to agent").option("--assign-seat", "Allow --assign to name a durable seat (a seat queue has no session watching it)").option("--expectation-id <id>", "Expectation metadata ID").option("--expectation-fingerprint <key>", "Expectation metadata fingerprint").option("--evidence-paths <paths>", "Comma-separated evidence paths").option("--origin-loop-id <id>", "Origin loop ID").option("--origin-run-id <id>", "Origin run ID").option("--expected <json-or-text>", "Expected value metadata").option("--observed <json-or-text>", "Observed value metadata").option("--acceptance <json-or-text>", "Acceptance metadata").action(async (opts) => {
21977
22421
  const globalOpts = program2.opts();
21978
22422
  opts.tags = opts.tags || opts.tag;
21979
22423
  opts.list = opts.list || opts.taskList;
@@ -21987,6 +22431,10 @@ function registerTaskCommands(program2) {
21987
22431
  try {
21988
22432
  const projectId2 = explicitProject ? await cloudResolveProjectRef(cloud, explicitProject) : undefined;
21989
22433
  const taskListId2 = opts.list ? await cloudResolveTaskListRef(cloud, opts.list, projectId2) : undefined;
22434
+ const plan = opts.plan ? await cloudResolvePlan(cloud, opts.plan, projectId2) : null;
22435
+ if (opts.plan && !plan) {
22436
+ throw new Error(`Could not resolve plan ID or slug: ${opts.plan}`);
22437
+ }
21990
22438
  cloudResult = await cloudUpsertTaskByFingerprint(cloud, {
21991
22439
  fingerprint: opts.fingerprint,
21992
22440
  title: opts.title,
@@ -21998,7 +22446,8 @@ function registerTaskCommands(program2) {
21998
22446
  metadata: buildExpectationMetadata(opts),
21999
22447
  working_dir: opts.workingDir ? resolve9(opts.workingDir) : process.cwd(),
22000
22448
  project_id: projectId2,
22001
- assigned_to: opts.assign
22449
+ assigned_to: opts.assign,
22450
+ plan_id: plan?.id
22002
22451
  });
22003
22452
  } catch (e) {
22004
22453
  handleError(e);
@@ -22020,6 +22469,7 @@ function registerTaskCommands(program2) {
22020
22469
  }
22021
22470
  return id;
22022
22471
  })() : undefined;
22472
+ const planId = opts.plan ? resolvePlanId(opts.plan) : undefined;
22023
22473
  let result;
22024
22474
  try {
22025
22475
  result = upsertTaskByFingerprint({
@@ -22034,6 +22484,7 @@ function registerTaskCommands(program2) {
22034
22484
  working_dir: opts.workingDir ? resolve9(opts.workingDir) : process.cwd(),
22035
22485
  project_id: projectId,
22036
22486
  assigned_to: opts.assign,
22487
+ plan_id: planId,
22037
22488
  agent_id: globalOpts.agent,
22038
22489
  session_id: globalOpts.session
22039
22490
  });
@@ -23027,6 +23478,44 @@ ${chalk3.cyan(sid)} ${statusColor(task2.status)} ${prioColor(task2.priority)} ${
23027
23478
  console.log(chalk3.green("Lock released."));
23028
23479
  }
23029
23480
  });
23481
+ program2.command("stale-lock-handoff <id>").description("Atomically transfer one exact stale lock by holder and locked_at version").requiredOption("--expected-holder <agent>", "Exact current locked_by value").requiredOption("--expected-lock-version <timestamp>", "Exact current locked_at value (canonical UTC)").requiredOption("--stale-after-seconds <seconds>", "Required lock age threshold; no default").requiredOption("--new-holder <agent>", "New holder; must match the authenticated/--agent identity").requiredOption("--reason <text>", "Non-empty audit reason").action(async (id, opts) => {
23482
+ const globalOpts = program2.opts();
23483
+ const taskId = normalizeExactTaskId(id);
23484
+ const actor = resolveClaimIdentity("handoff a stale lock on", globalOpts.agent);
23485
+ const staleAfterSeconds = Number(opts.staleAfterSeconds);
23486
+ const cloud = getTodosCloudClient();
23487
+ let receipt;
23488
+ try {
23489
+ receipt = cloud ? await cloudHandoffStaleTaskLock(cloud, {
23490
+ task_id: taskId,
23491
+ expected_holder: opts.expectedHolder,
23492
+ expected_lock_version: opts.expectedLockVersion,
23493
+ stale_after_seconds: staleAfterSeconds,
23494
+ new_holder: opts.newHolder,
23495
+ reason: opts.reason
23496
+ }) : handoffStaleTaskLock({
23497
+ task_id: taskId,
23498
+ actor,
23499
+ expected_holder: opts.expectedHolder,
23500
+ expected_lock_version: opts.expectedLockVersion,
23501
+ stale_after_seconds: staleAfterSeconds,
23502
+ new_holder: opts.newHolder,
23503
+ reason: opts.reason
23504
+ });
23505
+ } catch (e) {
23506
+ handleError(e);
23507
+ }
23508
+ if (globalOpts.json) {
23509
+ output({ receipt }, true);
23510
+ return;
23511
+ }
23512
+ console.log(chalk3.green(`Stale lock transferred on task ${escapeTerminalControls(receipt.task_id)}.`));
23513
+ console.log(` ${escapeTerminalControls(receipt.previous_holder)} @ ${escapeTerminalControls(receipt.previous_lock_version)}`);
23514
+ console.log(` -> ${escapeTerminalControls(receipt.new_holder)} @ ${escapeTerminalControls(receipt.new_lock_version)}`);
23515
+ console.log(` stale after ${receipt.stale_after_seconds}s (cutoff ${escapeTerminalControls(receipt.stale_cutoff)})`);
23516
+ console.log(` receipt ${escapeTerminalControls(receipt.receipt_id)}`);
23517
+ console.log(` reason ${escapeTerminalControls(receipt.reason)}`);
23518
+ });
23030
23519
  program2.command("delete <id>").description("Delete a task").action(async (id) => {
23031
23520
  const globalOpts = program2.opts();
23032
23521
  const cloud = getTodosCloudClient();
@@ -23211,6 +23700,7 @@ var init_task_commands = __esm(() => {
23211
23700
  init_agents();
23212
23701
  init_helpers();
23213
23702
  init_output_redaction();
23703
+ init_stale_lock_handoff();
23214
23704
  });
23215
23705
 
23216
23706
  // src/lib/plan-artifacts.ts
@@ -24198,6 +24688,58 @@ var init_plan_project_links = __esm(() => {
24198
24688
  init_tasks();
24199
24689
  });
24200
24690
 
24691
+ // src/storage/audit-history-import.ts
24692
+ function auditHistoryRowsAreFieldIdentical(left, right) {
24693
+ return AUDIT_HISTORY_FIELDS.every((field) => {
24694
+ const leftValue = field === "machine_id" ? left[field] ?? null : left[field];
24695
+ const rightValue = field === "machine_id" ? right[field] ?? null : right[field];
24696
+ return leftValue === rightValue;
24697
+ });
24698
+ }
24699
+ function divergentAuditHistoryReplayError(id) {
24700
+ return `${AUDIT_HISTORY_DIVERGENT_REPLAY}: immutable audit_history row ${id} differs from stored row`;
24701
+ }
24702
+ function forbiddenAuditHistoryTombstoneError(id) {
24703
+ return `${AUDIT_HISTORY_TOMBSTONE_FORBIDDEN}: audit_history tombstone ${id} is not allowed`;
24704
+ }
24705
+ function parseAuditHistoryImportFailure(message) {
24706
+ const divergentPrefix = `${AUDIT_HISTORY_DIVERGENT_REPLAY}: immutable audit_history row `;
24707
+ const divergentSuffix = " differs from stored row";
24708
+ if (message.startsWith(divergentPrefix) && message.endsWith(divergentSuffix)) {
24709
+ return {
24710
+ code: AUDIT_HISTORY_DIVERGENT_REPLAY,
24711
+ auditHistoryId: message.slice(divergentPrefix.length, -divergentSuffix.length),
24712
+ conflict: true,
24713
+ status: 409
24714
+ };
24715
+ }
24716
+ const tombstonePrefix = `${AUDIT_HISTORY_TOMBSTONE_FORBIDDEN}: audit_history tombstone `;
24717
+ const tombstoneSuffix = " is not allowed";
24718
+ if (message.startsWith(tombstonePrefix) && message.endsWith(tombstoneSuffix)) {
24719
+ return {
24720
+ code: AUDIT_HISTORY_TOMBSTONE_FORBIDDEN,
24721
+ auditHistoryId: message.slice(tombstonePrefix.length, -tombstoneSuffix.length),
24722
+ conflict: false,
24723
+ status: 400
24724
+ };
24725
+ }
24726
+ return null;
24727
+ }
24728
+ var AUDIT_HISTORY_DIVERGENT_REPLAY = "AUDIT_HISTORY_DIVERGENT_REPLAY", AUDIT_HISTORY_TOMBSTONE_FORBIDDEN = "AUDIT_HISTORY_TOMBSTONE_FORBIDDEN", AUDIT_HISTORY_FIELDS;
24729
+ var init_audit_history_import = __esm(() => {
24730
+ AUDIT_HISTORY_FIELDS = [
24731
+ "id",
24732
+ "task_id",
24733
+ "action",
24734
+ "field",
24735
+ "old_value",
24736
+ "new_value",
24737
+ "agent_id",
24738
+ "created_at",
24739
+ "machine_id"
24740
+ ];
24741
+ });
24742
+
24201
24743
  // src/storage/sqlite-snapshot.ts
24202
24744
  function exportSqliteTodosStorageSnapshot(db) {
24203
24745
  const d = db ?? getDatabase();
@@ -24231,9 +24773,12 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
24231
24773
  const existingTaskLists = d.query("SELECT id, project_id, slug FROM task_lists").all();
24232
24774
  result.errors.push(...validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists));
24233
24775
  }
24776
+ const auditImport = preflightAuditHistoryImport(d, snapshot.auditHistory, snapshot.tombstones ?? []);
24777
+ result.errors.push(...auditImport.errors);
24234
24778
  if (result.errors.length > 0)
24235
24779
  return result;
24236
- const applyRows = (objectType, table, columns, rows, updateClockColumn, afterUpsert) => {
24780
+ result.skipped += auditImport.identicalReplayCount;
24781
+ const applyRows = (objectType, table, columns, rows, updateClockColumn, acceptEqualClock = true, afterUpsert) => {
24237
24782
  for (const row of rows) {
24238
24783
  try {
24239
24784
  const record = asRecord(row);
@@ -24242,7 +24787,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
24242
24787
  result.skipped += 1;
24243
24788
  continue;
24244
24789
  }
24245
- const state = upsertById(d, table, columns, record, updateClockColumn);
24790
+ const state = upsertById(d, table, columns, record, updateClockColumn, acceptEqualClock);
24246
24791
  if (state === "inserted")
24247
24792
  result.inserted += 1;
24248
24793
  else if (state === "updated")
@@ -24259,19 +24804,81 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
24259
24804
  applyRows("project_machine_paths", "project_machine_paths", PROJECT_MACHINE_PATH_COLUMNS, snapshot.projectMachinePaths ?? [], "updated_at");
24260
24805
  applyRows("agents", "agents", AGENT_COLUMNS, snapshot.agents, "last_seen_at");
24261
24806
  applyRows("task_lists", "task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
24262
- applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at");
24807
+ applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at", false);
24263
24808
  applyRows("templates", "task_templates", TEMPLATE_COLUMNS, snapshot.templates);
24264
24809
  applyRows("template_tasks", "template_tasks", TEMPLATE_TASK_COLUMNS, snapshot.templateTasks ?? []);
24265
- applyRows("tasks", "tasks", TASK_COLUMNS, sortedTasks(snapshot.tasks), "updated_at", (row, changed) => {
24810
+ applyRows("tasks", "tasks", TASK_COLUMNS, sortedTasks(snapshot.tasks), "updated_at", true, (row, changed) => {
24266
24811
  if (changed && Array.isArray(row["tags"]) && typeof row["id"] === "string") {
24267
24812
  replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
24268
24813
  }
24269
24814
  });
24270
- applyRows("audit_history", "task_history", AUDIT_COLUMNS, snapshot.auditHistory);
24815
+ insertAuditHistoryRows(d, auditImport.rowsToInsert, result);
24271
24816
  applyTombstones(d, snapshot.tombstones ?? [], result);
24272
24817
  return result;
24273
24818
  }
24274
- function upsertById(db, table, columns, row, updateClockColumn) {
24819
+ function preflightAuditHistoryImport(db, rows, tombstones) {
24820
+ const errors = tombstones.filter((tombstone) => tombstone.object_type === "audit_history").map((tombstone) => forbiddenAuditHistoryTombstoneError(tombstone.object_id));
24821
+ const rowsToInsert = [];
24822
+ const seen = new Map;
24823
+ let identicalReplayCount = 0;
24824
+ for (const rawRow of rows) {
24825
+ try {
24826
+ const row = asRecord(rawRow);
24827
+ if (typeof row.id !== "string" || !row.id) {
24828
+ throw new Error("task_history row is missing id");
24829
+ }
24830
+ const prior = seen.get(row.id);
24831
+ if (prior) {
24832
+ if (auditHistoryRowsAreFieldIdentical(prior, row))
24833
+ identicalReplayCount += 1;
24834
+ else
24835
+ errors.push(divergentAuditHistoryReplayError(row.id));
24836
+ continue;
24837
+ }
24838
+ seen.set(row.id, row);
24839
+ const existing = getAuditHistoryById(db, row.id);
24840
+ if (!existing) {
24841
+ rowsToInsert.push(row);
24842
+ } else if (auditHistoryRowsAreFieldIdentical(existing, row)) {
24843
+ identicalReplayCount += 1;
24844
+ } else {
24845
+ errors.push(divergentAuditHistoryReplayError(row.id));
24846
+ }
24847
+ } catch (error) {
24848
+ errors.push(error instanceof Error ? error.message : String(error));
24849
+ }
24850
+ }
24851
+ return { rowsToInsert, identicalReplayCount, errors };
24852
+ }
24853
+ function insertAuditHistoryRows(db, rows, result) {
24854
+ for (const rawRow of rows) {
24855
+ try {
24856
+ const row = asRecord(rawRow);
24857
+ const presentColumns = AUDIT_COLUMNS.filter((column) => (column in row));
24858
+ if (!presentColumns.includes("id"))
24859
+ presentColumns.unshift("id");
24860
+ const placeholders = presentColumns.map(() => "?").join(", ");
24861
+ const values = presentColumns.map((column) => valueForColumn(column, row[column]));
24862
+ const changes = db.run(`INSERT OR IGNORE INTO task_history (${presentColumns.join(", ")}) VALUES (${placeholders})`, values).changes;
24863
+ if (changes > 0) {
24864
+ result.inserted += 1;
24865
+ continue;
24866
+ }
24867
+ const existing = getAuditHistoryById(db, String(row["id"]));
24868
+ if (existing && auditHistoryRowsAreFieldIdentical(existing, row)) {
24869
+ result.skipped += 1;
24870
+ } else {
24871
+ result.errors.push(divergentAuditHistoryReplayError(String(row["id"])));
24872
+ }
24873
+ } catch (error) {
24874
+ result.errors.push(error instanceof Error ? error.message : String(error));
24875
+ }
24876
+ }
24877
+ }
24878
+ function getAuditHistoryById(db, id) {
24879
+ return db.query(`SELECT ${AUDIT_COLUMNS.join(", ")} FROM task_history WHERE id = ? LIMIT 1`).get(id);
24880
+ }
24881
+ function upsertById(db, table, columns, row, updateClockColumn, acceptEqualClock = true) {
24275
24882
  const id = row["id"];
24276
24883
  if (typeof id !== "string" || !id)
24277
24884
  throw new Error(`${table} row is missing id`);
@@ -24283,7 +24890,7 @@ function upsertById(db, table, columns, row, updateClockColumn) {
24283
24890
  const values = presentColumns.map((column) => valueForColumn(column, row[column]));
24284
24891
  const updateColumns = presentColumns.filter((column) => column !== "id");
24285
24892
  const updateSet = updateColumns.map((column) => column === "version" ? `version = MAX(COALESCE(${table}.version, 0), excluded.version)` : `${column} = excluded.${column}`).join(", ");
24286
- const clockGuard = updateClockColumn && presentColumns.includes(updateClockColumn) ? ` WHERE ${table}.${updateClockColumn} IS NULL OR ${table}.${updateClockColumn} <= excluded.${updateClockColumn}` : "";
24893
+ const clockGuard = updateClockColumn && presentColumns.includes(updateClockColumn) ? ` WHERE ${table}.${updateClockColumn} IS NULL OR ${table}.${updateClockColumn} ${acceptEqualClock ? "<=" : "<"} excluded.${updateClockColumn}` : "";
24287
24894
  const sql = updateSet ? `INSERT INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders})
24288
24895
  ON CONFLICT(id) DO UPDATE SET ${updateSet}${clockGuard}` : `INSERT OR IGNORE INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders})`;
24289
24896
  const changes = db.run(sql, values).changes;
@@ -24368,7 +24975,7 @@ function tableForTombstone(objectType) {
24368
24975
  return "task_templates";
24369
24976
  if (objectType === "template_tasks")
24370
24977
  return "template_tasks";
24371
- return "task_history";
24978
+ throw new Error(`unsupported storage tombstone object_type: ${String(objectType)}`);
24372
24979
  }
24373
24980
  function listRows(db, table, columns) {
24374
24981
  return db.query(`SELECT ${columns.join(", ")} FROM ${table} ORDER BY id`).all();
@@ -24415,6 +25022,7 @@ var init_sqlite_snapshot = __esm(() => {
24415
25022
  init_tasks();
24416
25023
  init_templates();
24417
25024
  init_storage_tombstones();
25025
+ init_audit_history_import();
24418
25026
  PROJECT_COLUMNS = [
24419
25027
  "id",
24420
25028
  "name",
@@ -24679,6 +25287,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
24679
25287
  unlockTask(id, agentId, database());
24680
25288
  return true;
24681
25289
  },
25290
+ handoffStaleLock: (input) => handoffStaleTaskLock(input, database()),
24682
25291
  delete: (id) => deleteTask(id, database()),
24683
25292
  start: (id, agentId) => startTask(id, agentId, database()),
24684
25293
  complete: (id, agentId, options2) => completeTask(id, agentId, database(), options2),
@@ -24702,6 +25311,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
24702
25311
  get: (id) => getPlan(id, database()),
24703
25312
  list: (projectId) => listPlans(projectId, database()),
24704
25313
  update: (id, input) => updatePlan(id, input, database()),
25314
+ completeAtRevision: (id, expectedUpdatedAt) => completePlanAtRevision(id, expectedUpdatedAt, database()),
24705
25315
  delete: (id) => deletePlan(id, database())
24706
25316
  },
24707
25317
  planProjectLinks: {
@@ -25434,7 +26044,7 @@ function registerPlanTemplateCommands(program2) {
25434
26044
  if (!plan2) {
25435
26045
  handleError(new Error(`Plan not found: ${opts.show}`));
25436
26046
  }
25437
- const tasks2 = await cloudListTasks(cloud, { plan_id: plan2.id, include_subtasks: true });
26047
+ const tasks2 = await cloudListPlanTasks(cloud, plan2.id);
25438
26048
  if (globalOpts.json) {
25439
26049
  output({ plan: plan2, tasks: tasks2, artifact: null }, true);
25440
26050
  return;
@@ -29234,6 +29844,7 @@ __export(exports_project_commands, {
29234
29844
  registerProjectCommands: () => registerProjectCommands
29235
29845
  });
29236
29846
  import chalk5 from "chalk";
29847
+ import { readFileSync as readFileSync9, statSync as statSync6 } from "fs";
29237
29848
  import { basename as basename5, resolve as resolve13, sep as sep3 } from "path";
29238
29849
  function collectOption(value, previous = []) {
29239
29850
  return [...previous, value];
@@ -29427,18 +30038,45 @@ function registerProjectCommands(program2) {
29427
30038
  handleError(e);
29428
30039
  }
29429
30040
  });
29430
- program2.command("comment <id> <text>").alias("log-progress").description("Add a comment to a task (alias: log-progress, for recording intermediate progress)").option("--pct <percent>", "Progress percentage (0-100) to record alongside the note").action(async (id, text, opts) => {
30041
+ program2.command("comment <id> [text]").alias("log-progress").description("Add a comment to a task (alias: log-progress, for recording intermediate progress)").option("--file <path>", "Read comment text from a UTF-8 file").option("--pct <percent>", "Progress percentage (0-100) to record alongside the note").action(async (id, text, opts) => {
30042
+ const hasPositionalContent = text !== undefined;
30043
+ const hasFileContent = opts.file !== undefined;
30044
+ if (hasPositionalContent === hasFileContent) {
30045
+ handleError(new Error("Provide exactly one comment content source: positional text or --file <path>, not both."));
30046
+ }
30047
+ let content;
30048
+ if (opts.file !== undefined) {
30049
+ const commentFilePath = resolve13(opts.file);
30050
+ let isRegularFile = false;
30051
+ try {
30052
+ isRegularFile = statSync6(commentFilePath).isFile();
30053
+ } catch (error) {
30054
+ handleError(new Error(`Unable to read comment file "${opts.file}".`, { cause: error }));
30055
+ }
30056
+ if (!isRegularFile) {
30057
+ handleError(new Error(`Comment file "${opts.file}" must be a regular file.`));
30058
+ }
30059
+ try {
30060
+ content = readFileSync9(commentFilePath, "utf8");
30061
+ } catch (error) {
30062
+ handleError(new Error(`Unable to read comment file "${opts.file}".`, { cause: error }));
30063
+ }
30064
+ } else {
30065
+ content = text;
30066
+ }
30067
+ if (!content.trim()) {
30068
+ handleError(new Error("Comment content must not be empty."));
30069
+ }
29431
30070
  const globalOpts = program2.opts();
29432
30071
  const cloud = getTodosCloudClient();
29433
30072
  const resolvedId = await resolveTaskIdForCommand(id, cloud);
29434
- let content = text;
29435
30073
  let progressPct;
29436
30074
  if (opts.pct !== undefined) {
29437
30075
  const pct = parseInt(opts.pct, 10);
29438
30076
  if (!Number.isFinite(pct) || pct < 0 || pct > 100) {
29439
30077
  handleError(new Error("--pct must be a number between 0 and 100"));
29440
30078
  }
29441
- content = `[progress ${pct}%] ${text}`;
30079
+ content = `[progress ${pct}%] ${content}`;
29442
30080
  progressPct = pct;
29443
30081
  }
29444
30082
  const router = resolveWritableIdentity(globalOpts.agent);
@@ -30198,10 +30836,10 @@ Indexed ${result.index.files.length} file(s), ${result.index.total_symbols} symb
30198
30836
  program2.command("bridge-import <file>").description("Dry-run or apply a local hasna/todos bridge export bundle").option("--apply", "Apply the import. Defaults to dry-run.").option("--decrypt", "Decrypt an encrypted bridge export before importing").option("--resolve-conflicts", "Safely merge existing local tasks by filling blank fields, unioning tags, and recording unresolved divergences").action(async (file, opts) => {
30199
30837
  const globalOpts = program2.opts();
30200
30838
  try {
30201
- const { readFileSync: readFileSync9 } = await import("fs");
30839
+ const { readFileSync: readFileSync10 } = await import("fs");
30202
30840
  const { importLocalBridgeBundle: importLocalBridgeBundle2 } = await Promise.resolve().then(() => (init_local_bridge(), exports_local_bridge));
30203
30841
  const { decryptBridgeBundle: decryptBridgeBundle2, isEncryptedBridgeBundle: isEncryptedBridgeBundle2 } = await Promise.resolve().then(() => (init_local_encryption(), exports_local_encryption));
30204
- const parsed = JSON.parse(readFileSync9(resolve13(file), "utf-8"));
30842
+ const parsed = JSON.parse(readFileSync10(resolve13(file), "utf-8"));
30205
30843
  const bundle = isEncryptedBridgeBundle2(parsed) ? opts.decrypt ? decryptBridgeBundle2(parsed) : (() => {
30206
30844
  throw new Error("Bridge bundle is encrypted. Re-run with --decrypt and the configured key environment variable set.");
30207
30845
  })() : parsed;
@@ -30235,9 +30873,9 @@ Indexed ${result.index.files.length} file(s), ${result.index.total_symbols} symb
30235
30873
  program2.command("todos-md-import <file>").alias("markdown-import").alias("import-md").description("Dry-run or apply a local todos.md Markdown import").option("--apply", "Apply the import. Defaults to dry-run.").option("--resolve-conflicts", "Safely merge embedded bridge task conflicts while preserving local divergent fields").action(async (file, opts) => {
30236
30874
  const globalOpts = program2.opts();
30237
30875
  try {
30238
- const { readFileSync: readFileSync9 } = await import("fs");
30876
+ const { readFileSync: readFileSync10 } = await import("fs");
30239
30877
  const { importTodosMarkdown: importTodosMarkdown2 } = await Promise.resolve().then(() => (init_todos_md(), exports_todos_md));
30240
- const result = importTodosMarkdown2(readFileSync9(resolve13(file), "utf-8"), { dryRun: !opts.apply, conflictStrategy: opts.resolveConflicts ? "safe_merge" : "skip" });
30878
+ const result = importTodosMarkdown2(readFileSync10(resolve13(file), "utf-8"), { dryRun: !opts.apply, conflictStrategy: opts.resolveConflicts ? "safe_merge" : "skip" });
30241
30879
  const { emitLocalEventHooksQuiet: emitLocalEventHooksQuiet2 } = await Promise.resolve().then(() => (init_event_hooks(), exports_event_hooks));
30242
30880
  emitLocalEventHooksQuiet2({ type: "import.finished", payload: { file: resolve13(file), format: "todos.md", dry_run: result.dry_run, ok: result.ok, inserted: result.inserted, skipped: result.skipped, issues: result.issues.length } });
30243
30881
  if (globalOpts.json) {
@@ -30884,7 +31522,7 @@ async function findFreePort(start) {
30884
31522
  var DEFAULT_PORT = 19427;
30885
31523
 
30886
31524
  // src/lib/db-backup.ts
30887
- import { existsSync as existsSync15, copyFileSync, mkdirSync as mkdirSync8, readFileSync as readFileSync9, renameSync, statSync as statSync6, writeFileSync as writeFileSync7, unlinkSync } from "fs";
31525
+ import { existsSync as existsSync15, copyFileSync, mkdirSync as mkdirSync8, readFileSync as readFileSync10, renameSync, statSync as statSync7, writeFileSync as writeFileSync7, unlinkSync } from "fs";
30888
31526
  import { dirname as dirname7, join as join14, resolve as resolve14 } from "path";
30889
31527
  import { Database as Database2 } from "bun:sqlite";
30890
31528
  function resolveDbPath(dbPath) {
@@ -30913,7 +31551,7 @@ function backupDatabase(outputPath, sourcePath) {
30913
31551
  src.close();
30914
31552
  writeFileSync7(outputPath, image);
30915
31553
  const method = "file_copy";
30916
- const bytes = statSync6(outputPath).size;
31554
+ const bytes = statSync7(outputPath).size;
30917
31555
  return {
30918
31556
  schema_version: DB_BACKUP_SCHEMA,
30919
31557
  source_path: source,
@@ -32250,7 +32888,7 @@ __export(exports_local_extensions, {
32250
32888
  discoverLocalExtensions: () => discoverLocalExtensions
32251
32889
  });
32252
32890
  import { createHash as createHash9, createVerify } from "crypto";
32253
- import { existsSync as existsSync17, readdirSync as readdirSync3, readFileSync as readFileSync10, statSync as statSync7 } from "fs";
32891
+ import { existsSync as existsSync17, readdirSync as readdirSync3, readFileSync as readFileSync11, statSync as statSync8 } from "fs";
32254
32892
  import { basename as basename6, join as join16, resolve as resolve15 } from "path";
32255
32893
  function isObject(value) {
32256
32894
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
@@ -32332,7 +32970,7 @@ function normalizeManifest(input) {
32332
32970
  };
32333
32971
  }
32334
32972
  function parseJson(path) {
32335
- return JSON.parse(readFileSync10(path, "utf8"));
32973
+ return JSON.parse(readFileSync11(path, "utf8"));
32336
32974
  }
32337
32975
  function sha2564(bytes) {
32338
32976
  return `sha256:${createHash9("sha256").update(bytes).digest("hex")}`;
@@ -32512,11 +33150,11 @@ function inspectExtensionSource(source2) {
32512
33150
  const resolved = resolve15(source2);
32513
33151
  if (!existsSync17(resolved))
32514
33152
  throw new Error(`extension source not found: ${source2}`);
32515
- const stat = statSync7(resolved);
33153
+ const stat = statSync8(resolved);
32516
33154
  const manifestPath = stat.isDirectory() ? [join16(resolved, "todos.extension.json"), join16(resolved, "extension.json")].find(existsSync17) : resolved;
32517
33155
  if (!manifestPath)
32518
33156
  throw new Error(`extension directory ${source2} is missing todos.extension.json`);
32519
- const raw = readFileSync10(manifestPath);
33157
+ const raw = readFileSync11(manifestPath);
32520
33158
  const parsed = parseJson(manifestPath);
32521
33159
  const bundle = isObject(parsed) && isObject(parsed["manifest"]);
32522
33160
  const manifest = normalizeManifest(bundle ? parsed["manifest"] : parsed);
@@ -32618,7 +33256,7 @@ function projectExtensionSources(projectPath) {
32618
33256
  if (entry.startsWith("."))
32619
33257
  continue;
32620
33258
  const full = join16(extensionDir, entry);
32621
- if (statSync7(full).isDirectory() || entry.endsWith(".json"))
33259
+ if (statSync8(full).isDirectory() || entry.endsWith(".json"))
32622
33260
  candidates.push(full);
32623
33261
  }
32624
33262
  }
@@ -34258,7 +34896,7 @@ var init_postgres_sync = __esm(() => {
34258
34896
  });
34259
34897
 
34260
34898
  // src/storage/postgres-adapter.ts
34261
- import { randomUUID as randomUUID3 } from "crypto";
34899
+ import { randomUUID as randomUUID4 } from "crypto";
34262
34900
  function createPostgresTodosStorageAdapter(options) {
34263
34901
  const store = new PostgresJsonRecordStore(options);
34264
34902
  const adapter = {
@@ -34287,6 +34925,7 @@ function createPostgresTodosStorageAdapter(options) {
34287
34925
  getChangedSince: (since, filters) => getChangedSince(since, filters, store),
34288
34926
  lock: (id, agentId) => lockTask2(id, agentId, store),
34289
34927
  unlock: (id, agentId) => unlockTask2(id, agentId, store),
34928
+ handoffStaleLock: (input, context) => store.handoffStaleLock(input, context),
34290
34929
  getByFingerprint: (fingerprint) => store.getTaskByFingerprint(fingerprint)
34291
34930
  },
34292
34931
  dependencies: {
@@ -34323,6 +34962,7 @@ function createPostgresTodosStorageAdapter(options) {
34323
34962
  get: (id) => store.get("plans", id),
34324
34963
  list: async (projectId) => (await store.list("plans")).filter((plan) => projectId === undefined || plan.project_id === projectId).sort((a, b) => a.name.localeCompare(b.name)),
34325
34964
  update: (id, input) => updatePlan2(id, input, store),
34965
+ completeAtRevision: (id, expectedUpdatedAt, context) => store.completePlanAtRevision(id, expectedUpdatedAt, context),
34326
34966
  delete: (id, context) => store.deletePlan(id, context)
34327
34967
  },
34328
34968
  planProjectLinks: {
@@ -34432,6 +35072,91 @@ class PostgresJsonRecordStore {
34432
35072
  LIMIT 1`, [this.service, type, id]);
34433
35073
  return result.rows[0] ? payloadRecord2(result.rows[0].payload) : null;
34434
35074
  }
35075
+ async handoffStaleLock(input, context = {}) {
35076
+ const prepared = prepareStaleLockHandoff(input);
35077
+ const receipt = buildStaleLockHandoffReceipt(prepared);
35078
+ const history = staleLockHandoffHistory(receipt, this.machineId(context));
35079
+ await this.ensureSchema();
35080
+ const result = await this.options.client.query(`/* todos:stale-lock-handoff-atomic */ WITH
35081
+ target AS MATERIALIZED (
35082
+ SELECT payload
35083
+ FROM ${this.tableName}
35084
+ WHERE service = $1
35085
+ AND object_type = 'tasks'
35086
+ AND object_id = $2
35087
+ AND deleted_at IS NULL
35088
+ FOR UPDATE
35089
+ ),
35090
+ updated AS (
35091
+ UPDATE ${this.tableName} AS task_record
35092
+ SET payload = jsonb_set(
35093
+ jsonb_set(
35094
+ jsonb_set(
35095
+ jsonb_set(
35096
+ task_record.payload,
35097
+ '{locked_by}',
35098
+ to_jsonb($6::text),
35099
+ true
35100
+ ),
35101
+ '{locked_at}',
35102
+ to_jsonb($7::text),
35103
+ true
35104
+ ),
35105
+ '{updated_at}',
35106
+ to_jsonb($7::text),
35107
+ true
35108
+ ),
35109
+ '{version}',
35110
+ to_jsonb(COALESCE((task_record.payload->>'version')::integer, 0) + 1),
35111
+ true
35112
+ ),
35113
+ updated_at = $7::timestamptz,
35114
+ source_machine_id = $10,
35115
+ version = COALESCE(task_record.version, 0) + 1
35116
+ FROM target
35117
+ WHERE task_record.service = $1
35118
+ AND task_record.object_type = 'tasks'
35119
+ AND task_record.object_id = $2
35120
+ AND task_record.deleted_at IS NULL
35121
+ AND target.payload->>'locked_by' = $3
35122
+ AND target.payload->>'locked_at' = $4
35123
+ AND todos_try_timestamptz(target.payload->>'locked_at') < $5::timestamptz
35124
+ AND COALESCE(target.payload->>'status', '') NOT IN ('completed', 'failed', 'cancelled')
35125
+ RETURNING task_record.payload
35126
+ ),
35127
+ audit AS (
35128
+ INSERT INTO ${this.tableName} (
35129
+ service, object_type, object_id, payload, updated_at,
35130
+ deleted_at, source_machine_id, version
35131
+ )
35132
+ SELECT $1, 'audit_history', $8, $9::jsonb, $7::timestamptz,
35133
+ NULL, $10, NULL
35134
+ FROM updated
35135
+ RETURNING payload
35136
+ )
35137
+ SELECT
35138
+ (SELECT payload FROM target) AS current_payload,
35139
+ (SELECT payload FROM updated) AS updated_payload,
35140
+ (SELECT payload FROM audit) AS audit_payload`, [
35141
+ this.service,
35142
+ prepared.task_id,
35143
+ prepared.expected_holder,
35144
+ prepared.expected_lock_version,
35145
+ prepared.stale_cutoff,
35146
+ prepared.new_holder,
35147
+ prepared.operation_timestamp,
35148
+ receipt.receipt_id,
35149
+ jsonbParam(history),
35150
+ this.machineId(context)
35151
+ ]);
35152
+ const row = result.rows[0];
35153
+ if (!row?.current_payload)
35154
+ throw new TaskNotFoundError(prepared.task_id);
35155
+ if (!row.updated_payload || !row.audit_payload) {
35156
+ throwStaleLockHandoffConflict(payloadRecord2(row.current_payload), prepared);
35157
+ }
35158
+ return receipt;
35159
+ }
34435
35160
  async list(type) {
34436
35161
  return (await this.listRecords(type)).map((record) => record.payload);
34437
35162
  }
@@ -34701,6 +35426,28 @@ class PostgresJsonRecordStore {
34701
35426
  }
34702
35427
  return value;
34703
35428
  }
35429
+ async insertImmutableAuditHistory(value, context = {}) {
35430
+ await this.ensureSchema();
35431
+ const inserted = await this.options.client.query(`INSERT INTO ${this.tableName} (
35432
+ service, object_type, object_id, payload, updated_at,
35433
+ deleted_at, source_machine_id, version
35434
+ ) VALUES ($1, $2, $3, $4::jsonb, $5::timestamptz, NULL, $6, NULL)
35435
+ ON CONFLICT (service, object_type, object_id) DO NOTHING
35436
+ RETURNING object_id`, [
35437
+ this.service,
35438
+ "audit_history",
35439
+ value.id,
35440
+ jsonbParam(value),
35441
+ value.created_at,
35442
+ context.requestId ?? this.sourceMachineId ?? null
35443
+ ]);
35444
+ if (inserted.rows.length > 0)
35445
+ return "inserted";
35446
+ const existing = await this.get("audit_history", value.id);
35447
+ if (existing && auditHistoryRowsAreFieldIdentical(existing, value))
35448
+ return "identical";
35449
+ throw new Error(divergentAuditHistoryReplayError(value.id));
35450
+ }
34704
35451
  async upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context = {}) {
34705
35452
  const planIds = [...new Set(guardedPlanIds.filter(Boolean))].sort();
34706
35453
  if (planIds.length === 0)
@@ -34810,6 +35557,54 @@ class PostgresJsonRecordStore {
34810
35557
  throw new PlanNotFoundError(value.id);
34811
35558
  return payloadRecord2(row.payload);
34812
35559
  }
35560
+ async completePlanAtRevision(id, expectedUpdatedAt, context = {}) {
35561
+ await this.ensureSchema();
35562
+ const result = await this.options.client.query(`/* todos:complete-plan-revision-cas */ WITH next_clock AS (
35563
+ SELECT date_trunc(
35564
+ 'milliseconds',
35565
+ GREATEST(clock_timestamp(), ($3::text)::timestamptz + interval '2 milliseconds')
35566
+ ) AS completed_at
35567
+ ), stored AS (
35568
+ UPDATE ${this.tableName} AS record SET
35569
+ payload = record.payload || jsonb_build_object(
35570
+ 'status', 'completed',
35571
+ 'updated_at', to_char(
35572
+ next_clock.completed_at AT TIME ZONE 'UTC',
35573
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
35574
+ )
35575
+ ),
35576
+ updated_at = next_clock.completed_at,
35577
+ deleted_at = NULL,
35578
+ source_machine_id = COALESCE($4, record.source_machine_id),
35579
+ version = COALESCE(record.version, 0) + 1
35580
+ FROM next_clock
35581
+ WHERE record.service = $1
35582
+ AND record.object_type = 'plans'
35583
+ AND record.object_id = $2
35584
+ AND record.deleted_at IS NULL
35585
+ AND record.payload->>'updated_at' = $3::text
35586
+ AND record.payload->>'status' IS DISTINCT FROM 'completed'
35587
+ RETURNING record.payload
35588
+ )
35589
+ SELECT payload FROM stored`, [
35590
+ this.service,
35591
+ id,
35592
+ expectedUpdatedAt,
35593
+ context.requestId ?? this.sourceMachineId ?? null
35594
+ ]);
35595
+ const payload = result.rows[0]?.payload;
35596
+ if (payload)
35597
+ return { plan: payloadRecord2(payload), applied: true };
35598
+ const current = await this.get("plans", id);
35599
+ if (!current)
35600
+ throw new PlanNotFoundError(id);
35601
+ if (current.updated_at !== expectedUpdatedAt) {
35602
+ throw new PlanRevisionConflictError(id, expectedUpdatedAt, current.updated_at);
35603
+ }
35604
+ if (current.status === "completed")
35605
+ return { plan: current, applied: false };
35606
+ throw new PlanRevisionConflictError(id, expectedUpdatedAt, current.updated_at);
35607
+ }
34813
35608
  async createTemplateWithTasks(template, tasks, context = {}) {
34814
35609
  await this.ensureSchema();
34815
35610
  const records = [
@@ -35479,7 +36274,7 @@ async function createTask2(input, store, context) {
35479
36274
  const effectiveProjectId = linkedPlan?.project_id ?? requestedProjectId;
35480
36275
  const shortId = effectiveProjectId ? await nextTaskShortId2(effectiveProjectId, store, context) : null;
35481
36276
  const task = {
35482
- id: randomUUID3(),
36277
+ id: randomUUID4(),
35483
36278
  short_id: shortId,
35484
36279
  project_id: effectiveProjectId,
35485
36280
  parent_id: input.parent_id ?? null,
@@ -35725,7 +36520,7 @@ async function addVerification(input, store, context) {
35725
36520
  throw new Error(`Task not found: ${input.task_id}`);
35726
36521
  const timestamp2 = new Date().toISOString();
35727
36522
  const verification = {
35728
- id: randomUUID3(),
36523
+ id: randomUUID4(),
35729
36524
  task_id: input.task_id,
35730
36525
  command: input.command,
35731
36526
  status: input.status ?? "unknown",
@@ -35746,7 +36541,7 @@ async function addCommit(input, store, context) {
35746
36541
  throw new Error(`Task not found: ${input.task_id}`);
35747
36542
  const timestamp2 = new Date().toISOString();
35748
36543
  const commit = {
35749
- id: randomUUID3(),
36544
+ id: randomUUID4(),
35750
36545
  task_id: input.task_id,
35751
36546
  sha: input.sha,
35752
36547
  message: input.message ?? null,
@@ -35769,7 +36564,7 @@ async function addGitRef(input, store, context) {
35769
36564
  throw new Error(`Task not found: ${input.task_id}`);
35770
36565
  const timestamp2 = new Date().toISOString();
35771
36566
  const gitRef = {
35772
- id: randomUUID3(),
36567
+ id: randomUUID4(),
35773
36568
  task_id: input.task_id,
35774
36569
  ref_type: input.ref_type,
35775
36570
  name: input.name,
@@ -35839,7 +36634,7 @@ async function createProject2(input, store, context) {
35839
36634
  if (!derivedSlug || !taskListId)
35840
36635
  throw new Error("Project name and task-list slug must be non-empty");
35841
36636
  const project = {
35842
- id: randomUUID3(),
36637
+ id: randomUUID4(),
35843
36638
  name: input.name,
35844
36639
  path: input.path,
35845
36640
  description: input.description ?? null,
@@ -35871,7 +36666,7 @@ async function createPlan2(input, store, context) {
35871
36666
  store
35872
36667
  });
35873
36668
  return store.upsert("plans", {
35874
- id: randomUUID3(),
36669
+ id: randomUUID4(),
35875
36670
  slug,
35876
36671
  project_id: projectId,
35877
36672
  task_list_id: input.task_list_id ?? context?.taskListId ?? null,
@@ -35923,7 +36718,7 @@ async function registerAgent2(input, store, context) {
35923
36718
  }
35924
36719
  const timestamp2 = new Date().toISOString();
35925
36720
  const agent = {
35926
- id: existing?.id ?? randomUUID3().slice(0, 8),
36721
+ id: existing?.id ?? randomUUID4().slice(0, 8),
35927
36722
  name: canonicalName,
35928
36723
  description: input.description ?? existing?.description ?? null,
35929
36724
  role: input.role ?? existing?.role ?? null,
@@ -35996,7 +36791,7 @@ async function createTaskList2(input, store, context) {
35996
36791
  if (!slug)
35997
36792
  throw new Error("Invalid task-list slug \u2014 must be non-empty kebab-case");
35998
36793
  return store.upsert("task_lists", {
35999
- id: randomUUID3(),
36794
+ id: randomUUID4(),
36000
36795
  project_id: input.project_id ?? context?.projectId ?? null,
36001
36796
  slug,
36002
36797
  name: input.name,
@@ -36031,7 +36826,7 @@ async function updateTaskList2(id, input, store) {
36031
36826
  async function createTemplate2(input, store, context) {
36032
36827
  const timestamp2 = new Date().toISOString();
36033
36828
  const template = {
36034
- id: randomUUID3(),
36829
+ id: randomUUID4(),
36035
36830
  name: input.name,
36036
36831
  title_pattern: input.title_pattern,
36037
36832
  description: input.description ?? null,
@@ -36052,7 +36847,7 @@ async function createTemplate2(input, store, context) {
36052
36847
  }
36053
36848
  function buildTemplateTasks(templateId, inputs, timestamp2) {
36054
36849
  return inputs.map((input, position) => ({
36055
- id: randomUUID3(),
36850
+ id: randomUUID4(),
36056
36851
  template_id: templateId,
36057
36852
  position,
36058
36853
  title_pattern: input.title_pattern,
@@ -36085,7 +36880,7 @@ async function updateTemplate2(id, input, store) {
36085
36880
  }
36086
36881
  async function logTaskChange2(taskId, action, field, oldValue, newValue, agentId, store, context) {
36087
36882
  const entry2 = {
36088
- id: randomUUID3(),
36883
+ id: randomUUID4(),
36089
36884
  task_id: taskId,
36090
36885
  action,
36091
36886
  field: field ?? null,
@@ -36099,7 +36894,7 @@ async function logTaskChange2(taskId, action, field, oldValue, newValue, agentId
36099
36894
  }
36100
36895
  async function addComment2(input, store, context) {
36101
36896
  const comment = {
36102
- id: randomUUID3(),
36897
+ id: randomUUID4(),
36103
36898
  task_id: input.task_id,
36104
36899
  agent_id: input.agent_id ?? context?.agentId ?? null,
36105
36900
  session_id: input.session_id ?? context?.sessionId ?? null,
@@ -36141,6 +36936,11 @@ async function importSnapshot(snapshot, store, context) {
36141
36936
  result.errors.push(...validateSnapshotRoutingDestinationConflicts(snapshot.projects, snapshot.taskLists, existingProjects, existingTaskLists));
36142
36937
  if (result.errors.length > 0)
36143
36938
  return result;
36939
+ const auditHistory = await preflightAuditHistoryImport2(snapshot.auditHistory, snapshot.tombstones ?? [], store);
36940
+ result.errors.push(...auditHistory.errors);
36941
+ if (result.errors.length > 0)
36942
+ return result;
36943
+ result.skipped += auditHistory.identical;
36144
36944
  const entries = [
36145
36945
  ...snapshot.tasks.map((row) => ["tasks", row]),
36146
36946
  ...snapshot.projects.map((row) => ["projects", row]),
@@ -36149,9 +36949,20 @@ async function importSnapshot(snapshot, store, context) {
36149
36949
  ...snapshot.agents.map((row) => ["agents", row]),
36150
36950
  ...snapshot.taskLists.map((row) => ["task_lists", row]),
36151
36951
  ...snapshot.templates.map((row) => ["templates", row]),
36152
- ...(snapshot.templateTasks ?? []).map((row) => ["template_tasks", row]),
36153
- ...snapshot.auditHistory.map((row) => ["audit_history", row])
36952
+ ...(snapshot.templateTasks ?? []).map((row) => ["template_tasks", row])
36154
36953
  ];
36954
+ for (const row of auditHistory.rowsToInsert) {
36955
+ try {
36956
+ const outcome = await store.insertImmutableAuditHistory(row, context);
36957
+ if (outcome === "inserted")
36958
+ result.inserted += 1;
36959
+ else
36960
+ result.skipped += 1;
36961
+ } catch (error) {
36962
+ result.errors.push(error instanceof Error ? error.message : String(error));
36963
+ return result;
36964
+ }
36965
+ }
36155
36966
  for (const [type, row] of entries) {
36156
36967
  try {
36157
36968
  const existing = await store.get(type, row.id);
@@ -36185,6 +36996,32 @@ async function importSnapshot(snapshot, store, context) {
36185
36996
  }
36186
36997
  return result;
36187
36998
  }
36999
+ async function preflightAuditHistoryImport2(rows, tombstones, store) {
37000
+ const errors = tombstones.filter((tombstone) => tombstone.object_type === "audit_history").map((tombstone) => forbiddenAuditHistoryTombstoneError(tombstone.object_id));
37001
+ const rowsToInsert = [];
37002
+ const seen = new Map;
37003
+ let identical = 0;
37004
+ for (const row of rows) {
37005
+ const prior = seen.get(row.id);
37006
+ if (prior) {
37007
+ if (auditHistoryRowsAreFieldIdentical(prior, row))
37008
+ identical += 1;
37009
+ else
37010
+ errors.push(divergentAuditHistoryReplayError(row.id));
37011
+ continue;
37012
+ }
37013
+ seen.set(row.id, row);
37014
+ const existing = await store.get("audit_history", row.id);
37015
+ if (!existing) {
37016
+ rowsToInsert.push(row);
37017
+ } else if (auditHistoryRowsAreFieldIdentical(existing, row)) {
37018
+ identical += 1;
37019
+ } else {
37020
+ errors.push(divergentAuditHistoryReplayError(row.id));
37021
+ }
37022
+ }
37023
+ return { rowsToInsert, identical, errors };
37024
+ }
36188
37025
  async function requireRecord(type, id, store) {
36189
37026
  const record = await store.get(type, id);
36190
37027
  if (!record)
@@ -36291,9 +37128,11 @@ var init_postgres_adapter = __esm(() => {
36291
37128
  init_types();
36292
37129
  init_creator_identity();
36293
37130
  init_plan_project_link_contract();
37131
+ init_stale_lock_handoff();
36294
37132
  init_postgres_sync();
36295
37133
  init_integrity();
36296
37134
  init_redaction();
37135
+ init_audit_history_import();
36297
37136
  TASK_ORDER_BY = `ORDER BY ${TASK_ORDER_TIEBREAK}`;
36298
37137
  });
36299
37138
 
@@ -44645,7 +45484,7 @@ var exports_doctor = {};
44645
45484
  __export(exports_doctor, {
44646
45485
  runTodosDoctor: () => runTodosDoctor
44647
45486
  });
44648
- import { chmodSync as chmodSync2, copyFileSync as copyFileSync2, existsSync as existsSync18, mkdirSync as mkdirSync9, statSync as statSync8 } from "fs";
45487
+ import { chmodSync as chmodSync2, copyFileSync as copyFileSync2, existsSync as existsSync18, mkdirSync as mkdirSync9, statSync as statSync9 } from "fs";
44649
45488
  import { basename as basename7, dirname as dirname8, join as join17 } from "path";
44650
45489
  function tableExists3(db, table) {
44651
45490
  return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
@@ -44792,7 +45631,7 @@ function databasePermissionsAreUnsafe(dbPath) {
44792
45631
  if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
44793
45632
  return false;
44794
45633
  try {
44795
- return (statSync8(dbPath).mode & 63) !== 0;
45634
+ return (statSync9(dbPath).mode & 63) !== 0;
44796
45635
  } catch {
44797
45636
  return false;
44798
45637
  }
@@ -46297,6 +47136,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
46297
47136
  ProjectTaskListEnsureResult: projectTaskListEnsureResultSchema,
46298
47137
  ProjectTaskListRollbackResult: projectTaskListRollbackResultSchema,
46299
47138
  TaskComment: taskCommentSchema,
47139
+ StaleLockHandoffInput: staleLockHandoffInputSchema,
47140
+ StaleLockHandoffReceipt: staleLockHandoffReceiptSchema,
46300
47141
  TaskGitRef: taskGitRefSchema,
46301
47142
  Plan: planSchema,
46302
47143
  PlanProjectLinkReceipt: planProjectLinkReceiptSchema,
@@ -47504,6 +48345,51 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
47504
48345
  }
47505
48346
  }
47506
48347
  },
48348
+ "/v1/tasks/{id}/stale-lock-handoff": {
48349
+ post: {
48350
+ operationId: "handoffStaleTaskLock",
48351
+ summary: "Atomically transfer one exact stale task lock",
48352
+ 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.",
48353
+ parameters: [
48354
+ {
48355
+ name: "id",
48356
+ in: "path",
48357
+ required: true,
48358
+ schema: { type: "string", format: "uuid" },
48359
+ description: "Exact full task UUID. Short ids and prefixes are rejected."
48360
+ }
48361
+ ],
48362
+ requestBody: {
48363
+ required: true,
48364
+ content: {
48365
+ "application/json": {
48366
+ schema: { $ref: "#/components/schemas/StaleLockHandoffInput" }
48367
+ }
48368
+ }
48369
+ },
48370
+ responses: {
48371
+ "200": {
48372
+ content: {
48373
+ "application/json": {
48374
+ schema: {
48375
+ type: "object",
48376
+ additionalProperties: false,
48377
+ required: ["receipt"],
48378
+ properties: {
48379
+ receipt: { $ref: "#/components/schemas/StaleLockHandoffReceipt" }
48380
+ }
48381
+ }
48382
+ }
48383
+ }
48384
+ },
48385
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
48386
+ "403": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
48387
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
48388
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
48389
+ "501": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
48390
+ }
48391
+ }
48392
+ },
47507
48393
  "/v1/tasks/{id}/refs": {
47508
48394
  get: {
47509
48395
  operationId: "listTaskGitRefs",
@@ -48010,8 +48896,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
48010
48896
  "/v1/import": {
48011
48897
  post: {
48012
48898
  operationId: "importSnapshot",
48013
- summary: "Bulk-ingest a full or partial snapshot (idempotent upsert by id)",
48014
- description: "Upserts every record carried in the body by primary key. All record arrays are optional and default to []; a caller may backfill a single object type (e.g. just tasks) or a complete snapshot. Re-posting the same rows never duplicates. Requires the todos:write scope.",
48899
+ summary: "Bulk-ingest a snapshot or atomically complete one observed plan",
48900
+ description: "Upserts every snapshot record by primary key, or accepts exactly one planCompletions operation that changes only plan status under an expected_updated_at CAS. Snapshot records and planCompletions are mutually exclusive. Requires the todos:write scope.",
48015
48901
  requestBody: {
48016
48902
  required: true,
48017
48903
  content: {
@@ -48030,7 +48916,22 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
48030
48916
  templates: { type: "array", items: { type: "object" } },
48031
48917
  templateTasks: { type: "array", items: { $ref: "#/components/schemas/TemplateTask" } },
48032
48918
  auditHistory: { type: "array", items: { type: "object" } },
48033
- tombstones: { type: "array", items: { type: "object" } }
48919
+ tombstones: { type: "array", items: { type: "object" } },
48920
+ planCompletions: {
48921
+ type: "array",
48922
+ minItems: 1,
48923
+ maxItems: 1,
48924
+ items: {
48925
+ type: "object",
48926
+ additionalProperties: false,
48927
+ required: ["id", "expected_updated_at", "status"],
48928
+ properties: {
48929
+ id: { type: "string" },
48930
+ expected_updated_at: { type: "string", format: "date-time" },
48931
+ status: { type: "string", enum: ["completed"] }
48932
+ }
48933
+ }
48934
+ }
48034
48935
  }
48035
48936
  }
48036
48937
  }
@@ -48053,6 +48954,26 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
48053
48954
  skipped: { type: "number" },
48054
48955
  errors: { type: "array", items: { type: "string" } }
48055
48956
  }
48957
+ },
48958
+ planCompletions: {
48959
+ type: "array",
48960
+ items: {
48961
+ type: "object",
48962
+ required: [
48963
+ "id",
48964
+ "status",
48965
+ "expected_updated_at",
48966
+ "result_updated_at",
48967
+ "applied"
48968
+ ],
48969
+ properties: {
48970
+ id: { type: "string" },
48971
+ status: { type: "string", enum: ["completed"] },
48972
+ expected_updated_at: { type: "string", format: "date-time" },
48973
+ result_updated_at: { type: "string", format: "date-time" },
48974
+ applied: { type: "boolean" }
48975
+ }
48976
+ }
48056
48977
  }
48057
48978
  }
48058
48979
  }
@@ -48065,7 +48986,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
48065
48986
  }
48066
48987
  };
48067
48988
  }
48068
- var taskSchema, projectSchema, taskManifestBindingLookupRequestSchema, taskManifestBindingLookupResultSchema, taskManifestBindingLookupResponseSchema, taskListSchema, projectTaskListEnsureReceiptSchema, projectTaskListEnsureResultSchema, projectTaskListRollbackResultSchema, taskCommentSchema, taskGitRefSchema, planSchema, planProjectLinkReceiptSchema, planProjectLinkResultSchema, planProjectLinkRollbackResultSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
48989
+ var taskSchema, projectSchema, taskManifestBindingLookupRequestSchema, taskManifestBindingLookupResultSchema, taskManifestBindingLookupResponseSchema, taskListSchema, projectTaskListEnsureReceiptSchema, projectTaskListEnsureResultSchema, projectTaskListRollbackResultSchema, taskCommentSchema, staleLockHandoffInputSchema, staleLockHandoffReceiptSchema, taskGitRefSchema, planSchema, planProjectLinkReceiptSchema, planProjectLinkResultSchema, planProjectLinkRollbackResultSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
48069
48990
  var init_openapi = __esm(() => {
48070
48991
  init_package_version();
48071
48992
  init_types();
@@ -48084,6 +49005,8 @@ var init_openapi = __esm(() => {
48084
49005
  reason: { type: "string", nullable: true },
48085
49006
  tags: { type: "array", items: { type: "string" } },
48086
49007
  version: { type: "number" },
49008
+ locked_by: { type: "string", nullable: true },
49009
+ locked_at: { type: "string", format: "date-time", nullable: true },
48087
49010
  created_at: { type: "string" },
48088
49011
  updated_at: { type: "string" }
48089
49012
  }
@@ -48248,6 +49171,69 @@ var init_openapi = __esm(() => {
48248
49171
  created_at: { type: "string", format: "date-time" }
48249
49172
  }
48250
49173
  };
49174
+ staleLockHandoffInputSchema = {
49175
+ type: "object",
49176
+ additionalProperties: false,
49177
+ required: [
49178
+ "expected_holder",
49179
+ "expected_lock_version",
49180
+ "stale_after_seconds",
49181
+ "new_holder",
49182
+ "reason"
49183
+ ],
49184
+ properties: {
49185
+ expected_holder: { type: "string", minLength: 1 },
49186
+ expected_lock_version: {
49187
+ type: "string",
49188
+ format: "date-time",
49189
+ pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$",
49190
+ description: "Exact authoritative locked_at token read from the task; no default or normalization is applied."
49191
+ },
49192
+ stale_after_seconds: {
49193
+ type: "integer",
49194
+ minimum: 1,
49195
+ description: "Lock age threshold supplied by the caller. The lock must be strictly older at the CAS instant."
49196
+ },
49197
+ new_holder: {
49198
+ type: "string",
49199
+ minLength: 1,
49200
+ description: "Must match the agent bound to the authenticated API key."
49201
+ },
49202
+ reason: { type: "string", minLength: 1, maxLength: 4096 }
49203
+ }
49204
+ };
49205
+ staleLockHandoffReceiptSchema = {
49206
+ type: "object",
49207
+ additionalProperties: false,
49208
+ required: [
49209
+ "schema_version",
49210
+ "receipt_id",
49211
+ "task_id",
49212
+ "actor",
49213
+ "previous_holder",
49214
+ "previous_lock_version",
49215
+ "new_holder",
49216
+ "new_lock_version",
49217
+ "stale_after_seconds",
49218
+ "stale_cutoff",
49219
+ "reason",
49220
+ "created_at"
49221
+ ],
49222
+ properties: {
49223
+ schema_version: { type: "string", enum: ["todos.stale-lock-handoff.v1"] },
49224
+ receipt_id: { type: "string", format: "uuid" },
49225
+ task_id: { type: "string", format: "uuid" },
49226
+ actor: { type: "string" },
49227
+ previous_holder: { type: "string" },
49228
+ previous_lock_version: { type: "string", format: "date-time" },
49229
+ new_holder: { type: "string" },
49230
+ new_lock_version: { type: "string", format: "date-time" },
49231
+ stale_after_seconds: { type: "integer", minimum: 1 },
49232
+ stale_cutoff: { type: "string", format: "date-time" },
49233
+ reason: { type: "string" },
49234
+ created_at: { type: "string", format: "date-time" }
49235
+ }
49236
+ };
48251
49237
  taskGitRefSchema = {
48252
49238
  type: "object",
48253
49239
  additionalProperties: false,
@@ -48902,6 +49888,65 @@ function normalizeImportSnapshot(raw) {
48902
49888
  function countSnapshotRecords(s) {
48903
49889
  return s.tasks.length + s.projects.length + (s.projectMachinePaths?.length ?? 0) + s.plans.length + s.agents.length + s.taskLists.length + s.templates.length + s.templateTasks.length + s.auditHistory.length + (s.tombstones?.length ?? 0);
48904
49890
  }
49891
+ function validatePlanCompletionImports(raw) {
49892
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
49893
+ return { present: false, operations: [] };
49894
+ }
49895
+ const body2 = raw;
49896
+ if (!Object.prototype.hasOwnProperty.call(body2, "planCompletions")) {
49897
+ return { present: false, operations: [] };
49898
+ }
49899
+ if (!Array.isArray(body2["planCompletions"]) || body2["planCompletions"].length !== 1) {
49900
+ return {
49901
+ present: true,
49902
+ operations: [],
49903
+ error: "planCompletions must contain exactly one completion operation"
49904
+ };
49905
+ }
49906
+ const operation = body2["planCompletions"][0];
49907
+ if (!operation || typeof operation !== "object" || Array.isArray(operation)) {
49908
+ return { present: true, operations: [], error: "plan completion must be an object" };
49909
+ }
49910
+ const record = operation;
49911
+ const allowed = new Set(["id", "expected_updated_at", "status"]);
49912
+ const unknown = Object.keys(record).find((key2) => !allowed.has(key2));
49913
+ if (unknown) {
49914
+ return { present: true, operations: [], error: `unknown plan completion field: ${unknown}` };
49915
+ }
49916
+ if (typeof record["id"] !== "string" || !record["id"].trim()) {
49917
+ return { present: true, operations: [], error: "plan completion id must be a non-empty string" };
49918
+ }
49919
+ if (record["status"] !== "completed") {
49920
+ return { present: true, operations: [], error: "plan completion status must be completed" };
49921
+ }
49922
+ const expectedUpdatedAt = typeof record["expected_updated_at"] === "string" ? record["expected_updated_at"] : "";
49923
+ const timestampMatch = RFC3339_DATE_TIME.exec(expectedUpdatedAt);
49924
+ const parsedTimestamp = Date.parse(expectedUpdatedAt);
49925
+ if (!timestampMatch || Number.isNaN(parsedTimestamp)) {
49926
+ return {
49927
+ present: true,
49928
+ operations: [],
49929
+ error: "plan completion expected_updated_at must be an RFC 3339 date-time with an explicit offset"
49930
+ };
49931
+ }
49932
+ const [, year, month, day] = timestampMatch;
49933
+ const calendarProbe = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
49934
+ if (calendarProbe.getUTCFullYear() !== Number(year) || calendarProbe.getUTCMonth() !== Number(month) - 1 || calendarProbe.getUTCDate() !== Number(day)) {
49935
+ return {
49936
+ present: true,
49937
+ operations: [],
49938
+ error: "plan completion expected_updated_at names a date that does not exist"
49939
+ };
49940
+ }
49941
+ return {
49942
+ present: true,
49943
+ operations: [{
49944
+ id: record["id"],
49945
+ expected_updated_at: expectedUpdatedAt,
49946
+ status: "completed"
49947
+ }]
49948
+ };
49949
+ }
48905
49950
  async function handleV1Request(req, url, dependencies = {}) {
48906
49951
  const path = url.pathname;
48907
49952
  if (path !== "/v1" && !path.startsWith("/v1/"))
@@ -49083,6 +50128,45 @@ async function handleV1Request(req, url, dependencies = {}) {
49083
50128
  return error(405, `method ${method} not allowed on /v1/tasks`);
49084
50129
  }
49085
50130
  if (action) {
50131
+ if (action === "stale-lock-handoff") {
50132
+ if (method !== "POST") {
50133
+ return error(405, "method must be POST on /v1/tasks/:id/stale-lock-handoff");
50134
+ }
50135
+ const exactId = normalizeExactTaskId(id);
50136
+ if (!principal.agent) {
50137
+ return error(403, "stale-lock handoff requires an authenticated agent-bound key", {
50138
+ code: "STALE_LOCK_HANDOFF_ACTOR_MISMATCH"
50139
+ });
50140
+ }
50141
+ if (typeof store.tasks.handoffStaleLock !== "function") {
50142
+ return error(501, "stale-lock handoff is not supported by this storage backend");
50143
+ }
50144
+ const body3 = await readJson3(req) ?? {};
50145
+ const allowed = new Set([
50146
+ "expected_holder",
50147
+ "expected_lock_version",
50148
+ "stale_after_seconds",
50149
+ "new_holder",
50150
+ "reason"
50151
+ ]);
50152
+ const unknown = Object.keys(body3).find((key2) => !allowed.has(key2));
50153
+ if (unknown) {
50154
+ return error(400, `unknown stale-lock handoff field: ${unknown}`, {
50155
+ code: "STALE_LOCK_HANDOFF_INVALID_INPUT",
50156
+ field: unknown
50157
+ });
50158
+ }
50159
+ const receipt = await store.tasks.handoffStaleLock({
50160
+ task_id: exactId,
50161
+ actor: principal.agent,
50162
+ expected_holder: body3.expected_holder,
50163
+ expected_lock_version: body3.expected_lock_version,
50164
+ stale_after_seconds: body3.stale_after_seconds,
50165
+ new_holder: body3.new_holder,
50166
+ reason: body3.reason
50167
+ }, contextFromPrincipal(principal));
50168
+ return json5({ receipt });
50169
+ }
49086
50170
  if (action === "comments") {
49087
50171
  if (method === "GET") {
49088
50172
  if (!await store.tasks.get(id))
@@ -49838,10 +50922,57 @@ async function handleV1Request(req, url, dependencies = {}) {
49838
50922
  return error(400, "invalid JSON body");
49839
50923
  const snapshot = normalizeImportSnapshot(raw);
49840
50924
  const received = countSnapshotRecords(snapshot);
50925
+ const completionImports = validatePlanCompletionImports(raw);
50926
+ if (completionImports.present) {
50927
+ if (completionImports.error)
50928
+ return error(400, completionImports.error);
50929
+ if (received !== 0) {
50930
+ return error(400, "planCompletions cannot be combined with snapshot record arrays");
50931
+ }
50932
+ if (typeof store.plans.completeAtRevision !== "function") {
50933
+ return error(501, "atomic plan completion is not supported by this storage backend");
50934
+ }
50935
+ const operation = completionImports.operations[0];
50936
+ const completed = await store.plans.completeAtRevision(operation.id, operation.expected_updated_at, contextFromPrincipal(principal));
50937
+ return json5({
50938
+ result: {
50939
+ inserted: 0,
50940
+ updated: completed.applied ? 1 : 0,
50941
+ deleted: 0,
50942
+ skipped: completed.applied ? 0 : 1,
50943
+ errors: []
50944
+ },
50945
+ received: 1,
50946
+ planCompletions: [{
50947
+ id: operation.id,
50948
+ status: "completed",
50949
+ expected_updated_at: operation.expected_updated_at,
50950
+ result_updated_at: completed.plan.updated_at,
50951
+ applied: completed.applied
50952
+ }]
50953
+ });
50954
+ }
49841
50955
  if (received === 0) {
49842
50956
  return error(400, "empty snapshot: provide at least one record array (tasks/projects/plans/...)");
49843
50957
  }
50958
+ const forbiddenAuditTombstone = (snapshot.tombstones ?? []).find((tombstone) => tombstone.object_type === "audit_history");
50959
+ if (forbiddenAuditTombstone) {
50960
+ return error(400, forbiddenAuditHistoryTombstoneError(forbiddenAuditTombstone.object_id), {
50961
+ code: AUDIT_HISTORY_TOMBSTONE_FORBIDDEN,
50962
+ conflict: false,
50963
+ audit_history_id: forbiddenAuditTombstone.object_id
50964
+ });
50965
+ }
49844
50966
  const result = await store.sync.importSnapshot(snapshot, contextFromPrincipal(principal));
50967
+ const auditFailureMessage = result.errors.find((message) => parseAuditHistoryImportFailure(message) !== null);
50968
+ if (auditFailureMessage) {
50969
+ const failure = parseAuditHistoryImportFailure(auditFailureMessage);
50970
+ return error(failure.status, auditFailureMessage, {
50971
+ code: failure.code,
50972
+ conflict: failure.conflict,
50973
+ audit_history_id: failure.auditHistoryId
50974
+ });
50975
+ }
49845
50976
  return json5({ result, received });
49846
50977
  }
49847
50978
  return error(404, `unknown /v1 resource: ${resource ?? "(root)"}`);
@@ -49864,6 +50995,26 @@ async function handleV1Request(req, url, dependencies = {}) {
49864
50995
  if (e instanceof TaskNotFoundError) {
49865
50996
  return error(404, e.message, { code: TaskNotFoundError.code });
49866
50997
  }
50998
+ if (e instanceof StaleLockHandoffError) {
50999
+ 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;
51000
+ return error(status2, e.message, {
51001
+ code: e.code,
51002
+ conflict: status2 === 409,
51003
+ ...e.details
51004
+ });
51005
+ }
51006
+ if (e instanceof PlanNotFoundError) {
51007
+ return error(404, e.message, { code: PlanNotFoundError.code });
51008
+ }
51009
+ if (e instanceof PlanRevisionConflictError) {
51010
+ return error(409, e.message, {
51011
+ code: PlanRevisionConflictError.code,
51012
+ conflict: true,
51013
+ plan_id: e.planId,
51014
+ expected_updated_at: e.expectedUpdatedAt,
51015
+ current_updated_at: e.currentUpdatedAt
51016
+ });
51017
+ }
49867
51018
  if (e instanceof LockError)
49868
51019
  return error(409, e.message, { code: LockError.code });
49869
51020
  if (e instanceof TaskNotStartableError) {
@@ -49886,6 +51037,8 @@ var init_v1 = __esm(() => {
49886
51037
  init_redaction();
49887
51038
  init_project_task_list_ensure();
49888
51039
  init_plan_project_link();
51040
+ init_stale_lock_handoff();
51041
+ init_audit_history_import();
49889
51042
  JSON_HEADERS4 = { "Content-Type": "application/json" };
49890
51043
  RFC3339_DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})[Tt]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$/;
49891
51044
  });
@@ -51015,7 +52168,7 @@ var exports_mention_resolver = {};
51015
52168
  __export(exports_mention_resolver, {
51016
52169
  resolveMentions: () => resolveMentions
51017
52170
  });
51018
- import { existsSync as existsSync19, readdirSync as readdirSync4, readFileSync as readFileSync11, statSync as statSync9 } from "fs";
52171
+ import { existsSync as existsSync19, readdirSync as readdirSync4, readFileSync as readFileSync12, statSync as statSync10 } from "fs";
51019
52172
  import { basename as basename8, isAbsolute, join as join19, relative as relative5, resolve as resolve18, sep as sep5 } from "path";
51020
52173
  function blankResolution(parsed) {
51021
52174
  return {
@@ -51118,13 +52271,13 @@ function resolveFile(parsed, workspace) {
51118
52271
  resolution.warnings.push("file does not exist in the local workspace");
51119
52272
  return resolution;
51120
52273
  }
51121
- const stats = statSync9(absolutePath);
52274
+ const stats = statSync10(absolutePath);
51122
52275
  if (!stats.isFile()) {
51123
52276
  resolution.warnings.push("path exists but is not a file");
51124
52277
  return resolution;
51125
52278
  }
51126
52279
  if (parsed.line !== undefined) {
51127
- const lineCount = readFileSync11(absolutePath, "utf-8").split(/\r?\n/).length;
52280
+ const lineCount = readFileSync12(absolutePath, "utf-8").split(/\r?\n/).length;
51128
52281
  if (parsed.line < 1 || parsed.line > lineCount) {
51129
52282
  resolution.warnings.push(`line ${parsed.line} is outside the file range 1-${lineCount}`);
51130
52283
  return resolution;
@@ -51156,7 +52309,7 @@ function walkSourceFiles(root, current = root, files = []) {
51156
52309
  if (!entry2.isFile())
51157
52310
  continue;
51158
52311
  const extension = `.${basename8(entry2.name).split(".").pop() || ""}`;
51159
- if (SOURCE_EXTENSIONS.has(extension) && statSync9(absolutePath).size <= 512 * 1024) {
52312
+ if (SOURCE_EXTENSIONS.has(extension) && statSync10(absolutePath).size <= 512 * 1024) {
51160
52313
  files.push(absolutePath);
51161
52314
  }
51162
52315
  }
@@ -51177,7 +52330,7 @@ function resolveSymbol(parsed, workspace, maxMatches) {
51177
52330
  const pattern = symbolPattern(name);
51178
52331
  const matches = [];
51179
52332
  for (const file of walkSourceFiles(workspace)) {
51180
- const lines = readFileSync11(file, "utf-8").split(/\r?\n/);
52333
+ const lines = readFileSync12(file, "utf-8").split(/\r?\n/);
51181
52334
  for (let index = 0;index < lines.length; index += 1) {
51182
52335
  const line = lines[index];
51183
52336
  const found = pattern.exec(line);
@@ -53919,7 +55072,7 @@ __export(exports_release_compatibility, {
53919
55072
  createReleaseCompatibilityReport: () => createReleaseCompatibilityReport,
53920
55073
  LOCAL_RELEASE_COMPATIBILITY_SCHEMA_VERSION: () => LOCAL_RELEASE_COMPATIBILITY_SCHEMA_VERSION
53921
55074
  });
53922
- import { readFileSync as readFileSync12 } from "fs";
55075
+ import { readFileSync as readFileSync13 } from "fs";
53923
55076
  import { join as join20, resolve as resolve19 } from "path";
53924
55077
  import { Database as Database3 } from "bun:sqlite";
53925
55078
  function pass(id, message, details) {
@@ -53932,7 +55085,7 @@ function warn(id, message, details) {
53932
55085
  return { id, status: "warning", message, details };
53933
55086
  }
53934
55087
  function readPackageJson2(root) {
53935
- return JSON.parse(readFileSync12(join20(root, "package.json"), "utf8"));
55088
+ return JSON.parse(readFileSync13(join20(root, "package.json"), "utf8"));
53936
55089
  }
53937
55090
  function sortedKeys(value) {
53938
55091
  return Object.keys(value ?? {}).sort((left, right) => left.localeCompare(right));
@@ -59902,7 +61055,7 @@ __export(exports_verification_providers, {
59902
61055
  getVerificationRecord: () => getVerificationRecord,
59903
61056
  discoverVerificationProviderCapabilities: () => discoverVerificationProviderCapabilities
59904
61057
  });
59905
- import { existsSync as existsSync20, readFileSync as readFileSync13 } from "fs";
61058
+ import { existsSync as existsSync20, readFileSync as readFileSync14 } from "fs";
59906
61059
  function normalizeName6(name) {
59907
61060
  const normalized = name.trim().toLowerCase();
59908
61061
  if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(normalized)) {
@@ -60054,7 +61207,7 @@ Timed out after ${provider.timeout_ms}ms`);
60054
61207
  };
60055
61208
  }
60056
61209
  function runCiLogProvider(input) {
60057
- const text = input.log_text ?? (input.log_path && existsSync20(input.log_path) ? readFileSync13(input.log_path, "utf-8") : "");
61210
+ const text = input.log_text ?? (input.log_path && existsSync20(input.log_path) ? readFileSync14(input.log_path, "utf-8") : "");
60058
61211
  return {
60059
61212
  status: classifyLog(text),
60060
61213
  attempts: 1,
@@ -62357,7 +63510,7 @@ __export(exports_local_backups, {
62357
63510
  LOCAL_BACKUP_CHECKSUM_ALGORITHM: () => LOCAL_BACKUP_CHECKSUM_ALGORITHM
62358
63511
  });
62359
63512
  import { createHash as createHash14 } from "crypto";
62360
- import { readFileSync as readFileSync14, writeFileSync as writeFileSync8 } from "fs";
63513
+ import { readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "fs";
62361
63514
  import { dirname as dirname9, resolve as resolve20 } from "path";
62362
63515
  import { mkdirSync as mkdirSync10 } from "fs";
62363
63516
  function stableJson2(value) {
@@ -62467,7 +63620,7 @@ function writeLocalBackupFile(backup, outputPath) {
62467
63620
  return path;
62468
63621
  }
62469
63622
  function readLocalBackupFile(path) {
62470
- return JSON.parse(readFileSync14(resolve20(path), "utf-8"));
63623
+ return JSON.parse(readFileSync15(resolve20(path), "utf-8"));
62471
63624
  }
62472
63625
  function verifyLocalBackup(value, options = {}, db) {
62473
63626
  const verifiedAt = options.verified_at ?? now();
@@ -63647,7 +64800,7 @@ __export(exports_agent_replay_simulator, {
63647
64800
  renderAgentReplaySimulationMarkdown: () => renderAgentReplaySimulationMarkdown
63648
64801
  });
63649
64802
  import { createHash as createHash16 } from "crypto";
63650
- import { readFileSync as readFileSync15 } from "fs";
64803
+ import { readFileSync as readFileSync16 } from "fs";
63651
64804
  function isObject2(value) {
63652
64805
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
63653
64806
  }
@@ -63880,7 +65033,7 @@ function simulateAgentReplay(input, options = {}) {
63880
65033
  };
63881
65034
  }
63882
65035
  function simulateAgentReplayFile(path, options = {}) {
63883
- const parsed = JSON.parse(readFileSync15(path, "utf8"));
65036
+ const parsed = JSON.parse(readFileSync16(path, "utf8"));
63884
65037
  return simulateAgentReplay(parsed, options);
63885
65038
  }
63886
65039
  function renderAgentReplaySimulationMarkdown(simulation) {
@@ -69219,7 +70372,7 @@ __export(exports_environment_snapshots, {
69219
70372
  captureEnvironmentSnapshot: () => captureEnvironmentSnapshot
69220
70373
  });
69221
70374
  import { createHash as createHash18 } from "crypto";
69222
- import { existsSync as existsSync21, readFileSync as readFileSync16, statSync as statSync10 } from "fs";
70375
+ import { existsSync as existsSync21, readFileSync as readFileSync17, statSync as statSync11 } from "fs";
69223
70376
  import { hostname as hostname2, platform, arch } from "os";
69224
70377
  import { dirname as dirname10, join as join22, resolve as resolve21 } from "path";
69225
70378
  import { tmpdir as tmpdir4 } from "os";
@@ -69230,10 +70383,10 @@ function fileRecord(root, relativePath) {
69230
70383
  const path = join22(root, relativePath);
69231
70384
  if (!existsSync21(path))
69232
70385
  return null;
69233
- const stat = statSync10(path);
70386
+ const stat = statSync11(path);
69234
70387
  if (!stat.isFile())
69235
70388
  return null;
69236
- const content = readFileSync16(path);
70389
+ const content = readFileSync17(path);
69237
70390
  return { path: relativePath, sha256: sha2567(content), size_bytes: content.length };
69238
70391
  }
69239
70392
  function manifestRecord(root, relativePath) {
@@ -72279,7 +73432,7 @@ __export(exports_config_serve_commands, {
72279
73432
  registerConfigServeCommands: () => registerConfigServeCommands
72280
73433
  });
72281
73434
  import chalk7 from "chalk";
72282
- import { existsSync as existsSync23, mkdirSync as mkdirSync12, readFileSync as readFileSync17, writeFileSync as writeFileSync10 } from "fs";
73435
+ import { existsSync as existsSync23, mkdirSync as mkdirSync12, readFileSync as readFileSync18, writeFileSync as writeFileSync10 } from "fs";
72283
73436
  import { dirname as dirname12, join as join24 } from "path";
72284
73437
  function registerConfigServeCommands(program2) {
72285
73438
  program2.command("config").description("View or update configuration").option("--get <key>", "Get a config value").option("--set <key=value>", "Set a config value (e.g. completion_guard.enabled=true)").action((opts) => {
@@ -72310,7 +73463,7 @@ function registerConfigServeCommands(program2) {
72310
73463
  }
72311
73464
  let config2 = {};
72312
73465
  try {
72313
- config2 = JSON.parse(readFileSync17(configPath, "utf-8"));
73466
+ config2 = JSON.parse(readFileSync18(configPath, "utf-8"));
72314
73467
  } catch {}
72315
73468
  const keys = key2.split(".");
72316
73469
  let obj = config2;
@@ -72450,7 +73603,7 @@ function registerConfigServeCommands(program2) {
72450
73603
  redaction.command("scan [text]").description("Scan text or a file for secret-like values without printing values").option("--file <path>", "File to scan").action(async (text2, opts) => {
72451
73604
  const globalOpts = program2.opts();
72452
73605
  const { listSecretFindings: listSecretFindings2 } = await Promise.resolve().then(() => (init_redaction(), exports_redaction));
72453
- const value = opts.file ? readFileSync17(opts.file, "utf-8") : text2 || "";
73606
+ const value = opts.file ? readFileSync18(opts.file, "utf-8") : text2 || "";
72454
73607
  const findings = listSecretFindings2(value);
72455
73608
  if (globalOpts.json) {
72456
73609
  output({ ok: findings.length === 0, findings }, true);
@@ -74016,7 +75169,7 @@ __export(exports_task_route_sources, {
74016
75169
  });
74017
75170
  import { Database as Database4 } from "bun:sqlite";
74018
75171
  import { createHash as createHash19 } from "crypto";
74019
- import { existsSync as existsSync25, readdirSync as readdirSync5, statSync as statSync11 } from "fs";
75172
+ import { existsSync as existsSync25, readdirSync as readdirSync5, statSync as statSync12 } from "fs";
74020
75173
  import { basename as basename10, dirname as dirname14, join as join26, resolve as resolve22 } from "path";
74021
75174
  function normalizePath6(input) {
74022
75175
  return resolve22(input);
@@ -74092,7 +75245,7 @@ function discoverStoresUnderRoot(sourceRoot) {
74092
75245
  }
74093
75246
  let rootStat;
74094
75247
  try {
74095
- rootStat = statSync11(rootPath);
75248
+ rootStat = statSync12(rootPath);
74096
75249
  } catch (error2) {
74097
75250
  const ref = createStoreRef(join26(rootPath, TODO_STORE_RELATIVE_PATH));
74098
75251
  errors2.push({
@@ -74825,7 +75978,7 @@ __export(exports_query_commands, {
74825
75978
  registerQueryCommands: () => registerQueryCommands
74826
75979
  });
74827
75980
  import chalk9 from "chalk";
74828
- import { readFileSync as readFileSync18, writeFileSync as writeFileSync12 } from "fs";
75981
+ import { readFileSync as readFileSync19, writeFileSync as writeFileSync12 } from "fs";
74829
75982
  function parseJsonObjectOption2(value, label) {
74830
75983
  if (!value)
74831
75984
  return;
@@ -75407,8 +76560,29 @@ No task claimed (nothing available).`));
75407
76560
  handleError(new Error("Failed to assign"));
75408
76561
  }
75409
76562
  });
75410
- program2.command("unassign <id>").description("Remove task assignment").option("-j, --json", "Output as JSON").action((id, opts) => {
76563
+ program2.command("unassign <id>").description("Remove task assignment").option("-j, --json", "Output as JSON").action(async (id, opts) => {
75411
76564
  const globalOpts = program2.opts();
76565
+ const cloud = getTodosCloudClient();
76566
+ if (cloud) {
76567
+ try {
76568
+ const resolvedId2 = await resolveTaskIdForCommand(id, cloud);
76569
+ const task4 = await cloudGetTask(cloud, resolvedId2);
76570
+ if (!task4) {
76571
+ throw new Error(`Task not found: ${id}`);
76572
+ }
76573
+ const updated = await cloudUpdateTask(cloud, resolvedId2, {
76574
+ assigned_to: null
76575
+ });
76576
+ if (opts.json || globalOpts.json) {
76577
+ console.log(JSON.stringify(updated));
76578
+ return;
76579
+ }
76580
+ console.log(chalk9.green(`Unassigned: ${formatTaskLine(updated)}`));
76581
+ } catch (error2) {
76582
+ handleError(error2);
76583
+ }
76584
+ return;
76585
+ }
75412
76586
  const resolvedId = resolveTaskId(id);
75413
76587
  const db = getDatabase();
75414
76588
  const task3 = getTask(resolvedId, db);
@@ -75845,13 +77019,13 @@ Findings`));
75845
77019
  try {
75846
77020
  const db = getDatabase();
75847
77021
  const row = db.query("SELECT COUNT(*) as count FROM tasks").get();
75848
- const { statSync: statSync12 } = await import("fs");
77022
+ const { statSync: statSync13 } = await import("fs");
75849
77023
  const { join: join27 } = await import("path");
75850
77024
  const { getHomeDir: getHomeDir2 } = await Promise.resolve().then(() => (init_sync_utils(), exports_sync_utils));
75851
77025
  const dbPath = process.env["HASNA_TODOS_DB_PATH"] || process.env["TODOS_DB_PATH"] || join27(getHomeDir2(), ".hasna", "todos", "todos.db");
75852
77026
  let size = "unknown";
75853
77027
  try {
75854
- size = `${(statSync12(dbPath).size / 1024 / 1024).toFixed(1)} MB`;
77028
+ size = `${(statSync13(dbPath).size / 1024 / 1024).toFixed(1)} MB`;
75855
77029
  } catch {}
75856
77030
  checks.push({ name: "Database", ok: true, message: `${row.count} tasks \xB7 ${size} \xB7 ${chalk9.dim(dbPath)}` });
75857
77031
  } catch (e) {
@@ -76568,7 +77742,7 @@ Findings`));
76568
77742
  const sessionId = opts.session || globalOpts.session || undefined;
76569
77743
  try {
76570
77744
  if (opts.import) {
76571
- const bundle = JSON.parse(readFileSync18(opts.import, "utf-8"));
77745
+ const bundle = JSON.parse(readFileSync19(opts.import, "utf-8"));
76572
77746
  const result = importHandoffBundle(bundle, { apply: opts.apply }, db);
76573
77747
  if (opts.json || globalOpts.json) {
76574
77748
  console.log(JSON.stringify(result));
@@ -76942,7 +78116,7 @@ Findings`));
76942
78116
  });
76943
78117
  calendar.command("import <path>").description("Import VEVENT entries from an ICS file as local imported calendar items").option("-j, --json", "Output JSON").action((path, opts) => {
76944
78118
  try {
76945
- const result = importCalendarIcs(readFileSync18(path, "utf-8"));
78119
+ const result = importCalendarIcs(readFileSync19(path, "utf-8"));
76946
78120
  if (opts.json || program2.opts().json) {
76947
78121
  output(result, true);
76948
78122
  return;
@@ -77093,7 +78267,7 @@ Findings`));
77093
78267
  });
77094
78268
  board.command("import <path>").description("Import local board definitions from a JSON bundle").option("-j, --json", "Output JSON").action((path, opts) => {
77095
78269
  try {
77096
- const bundle = JSON.parse(readFileSync18(path, "utf-8"));
78270
+ const bundle = JSON.parse(readFileSync19(path, "utf-8"));
77097
78271
  const result = importTaskBoardBundle(bundle);
77098
78272
  if (opts.json || program2.opts().json) {
77099
78273
  output(result, true);
@@ -77469,7 +78643,7 @@ Findings`));
77469
78643
  const { importExternalIssues: importExternalIssues2 } = await Promise.resolve().then(() => (init_external_issue_importers(), exports_external_issue_importers));
77470
78644
  let body2 = text2 || "";
77471
78645
  if (opts.file)
77472
- body2 = readFileSync18(opts.file, "utf-8");
78646
+ body2 = readFileSync19(opts.file, "utf-8");
77473
78647
  if (!body2 && !opts.url && !process.stdin.isTTY)
77474
78648
  body2 = await Bun.stdin.text();
77475
78649
  if (!body2.trim() && !opts.url) {
@@ -77526,7 +78700,7 @@ Findings`));
77526
78700
  } = await Promise.resolve().then(() => (init_tester_issue_reports(), exports_tester_issue_reports));
77527
78701
  let body2 = jsonText || "";
77528
78702
  if (opts.file)
77529
- body2 = readFileSync18(opts.file, "utf-8");
78703
+ body2 = readFileSync19(opts.file, "utf-8");
77530
78704
  if (!body2 && !process.stdin.isTTY)
77531
78705
  body2 = await Bun.stdin.text();
77532
78706
  if (!body2.trim()) {
@@ -77569,11 +78743,11 @@ Findings`));
77569
78743
  const inbox = program2.command("inbox").description("Capture local inbox items from pasted errors, CI logs, git context, files, or GitHub issue URLs");
77570
78744
  inbox.command("add [text]").description("Create a local inbox item and linked task from text, stdin, or a file").option("--file <path>", "Read captured context from a file").option("--source-type <type>", "pasted_error, ci_log, git_context, github_issue, file, or other").option("--source-name <name>", "Human-readable source name").option("--source-url <url>", "Source URL, including GitHub issue URLs").option("--title <title>", "Task/inbox title").option("--priority <priority>", "Task priority").option("--tags <tags>", "Comma-separated extra tags").option("--metadata <json>", "Additional JSON metadata").option("--no-task", "Only store inbox item; do not create a linked task").option("-j, --json", "Output as JSON").action(async (text2, opts) => {
77571
78745
  const globalOpts = program2.opts();
77572
- const { readFileSync: readFileSync19 } = await import("fs");
78746
+ const { readFileSync: readFileSync20 } = await import("fs");
77573
78747
  const { createInboxItem: createInboxItem2 } = await Promise.resolve().then(() => (init_inbox(), exports_inbox));
77574
78748
  let body2 = text2 || "";
77575
78749
  if (opts.file)
77576
- body2 = readFileSync19(opts.file, "utf-8");
78750
+ body2 = readFileSync20(opts.file, "utf-8");
77577
78751
  if (!body2 && !process.stdin.isTTY)
77578
78752
  body2 = await Bun.stdin.text();
77579
78753
  if (!body2.trim()) {
@@ -77633,11 +78807,11 @@ ${diff}` : null].filter(Boolean).join(`
77633
78807
  });
77634
78808
  inbox.command("parse [text]").description("Preview or apply deterministic local natural-language task intake").option("--file <path>", "Read natural-language input from a file").option("--priority <priority>", "Default priority for parsed tasks", "medium").option("--project <id>", "Project ID for applied tasks").option("--list <id>", "Task list ID for applied tasks").option("--reference-date <iso>", "Reference date for due today/tomorrow/next week").option("--apply", "Create parsed tasks; default is dry-run preview").option("-j, --json", "Output as JSON").action(async (text2, opts) => {
77635
78809
  const globalOpts = program2.opts();
77636
- const { readFileSync: readFileSync19 } = await import("fs");
78810
+ const { readFileSync: readFileSync20 } = await import("fs");
77637
78811
  const { previewNaturalLanguageIntake: previewNaturalLanguageIntake2 } = await Promise.resolve().then(() => (init_natural_language_intake(), exports_natural_language_intake));
77638
78812
  let body2 = text2 || "";
77639
78813
  if (opts.file)
77640
- body2 = readFileSync19(opts.file, "utf-8");
78814
+ body2 = readFileSync20(opts.file, "utf-8");
77641
78815
  if (!body2 && !process.stdin.isTTY)
77642
78816
  body2 = await Bun.stdin.text();
77643
78817
  if (!body2.trim()) {
@@ -77964,7 +79138,7 @@ __export(exports_mcp_hooks_commands, {
77964
79138
  });
77965
79139
  import chalk10 from "chalk";
77966
79140
  import { execSync as execSync3 } from "child_process";
77967
- import { existsSync as existsSync26, readFileSync as readFileSync19, writeFileSync as writeFileSync13, mkdirSync as mkdirSync14, chmodSync as chmodSync3 } from "fs";
79141
+ import { existsSync as existsSync26, readFileSync as readFileSync20, writeFileSync as writeFileSync13, mkdirSync as mkdirSync14, chmodSync as chmodSync3 } from "fs";
77968
79142
  import { dirname as dirname15, join as join27 } from "path";
77969
79143
  function getMcpBinaryPath() {
77970
79144
  try {
@@ -77981,7 +79155,7 @@ function readJsonFile2(path) {
77981
79155
  if (!existsSync26(path))
77982
79156
  return {};
77983
79157
  try {
77984
- return JSON.parse(readFileSync19(path, "utf-8"));
79158
+ return JSON.parse(readFileSync20(path, "utf-8"));
77985
79159
  } catch {
77986
79160
  return {};
77987
79161
  }
@@ -77996,7 +79170,7 @@ function writeJsonFile2(path, data) {
77996
79170
  function readTomlFile(path) {
77997
79171
  if (!existsSync26(path))
77998
79172
  return "";
77999
- return readFileSync19(path, "utf-8");
79173
+ return readFileSync20(path, "utf-8");
78000
79174
  }
78001
79175
  function writeTomlFile(path, content) {
78002
79176
  const dir = dirname15(path);
@@ -79187,7 +80361,7 @@ Artifacts:`));
79187
80361
  const hookPath = `${gitDir}/hooks/post-commit`;
79188
80362
  const marker = "# todos-auto-link";
79189
80363
  if (existsSync26(hookPath)) {
79190
- const existing = readFileSync19(hookPath, "utf-8");
80364
+ const existing = readFileSync20(hookPath, "utf-8");
79191
80365
  if (existing.includes(marker)) {
79192
80366
  console.log(chalk10.yellow("Hook already installed."));
79193
80367
  return;
@@ -79217,7 +80391,7 @@ $(dirname "$0")/../../scripts/post-commit-hook.sh
79217
80391
  console.log(chalk10.dim("No post-commit hook found."));
79218
80392
  return;
79219
80393
  }
79220
- const content = readFileSync19(hookPath, "utf-8");
80394
+ const content = readFileSync20(hookPath, "utf-8");
79221
80395
  if (!content.includes(marker)) {
79222
80396
  console.log(chalk10.dim("Hook not managed by todos."));
79223
80397
  return;
@@ -79468,7 +80642,7 @@ var STDIN_SENTINEL = "-";
79468
80642
  var init_delegation_brief = () => {};
79469
80643
 
79470
80644
  // src/lib/delegation-policy.ts
79471
- import { readFileSync as readFileSync20 } from "fs";
80645
+ import { readFileSync as readFileSync21 } from "fs";
79472
80646
  import { homedir as homedir4 } from "os";
79473
80647
  import { join as join28 } from "path";
79474
80648
  function defaultDelegationEmbargoPath() {
@@ -79476,7 +80650,7 @@ function defaultDelegationEmbargoPath() {
79476
80650
  }
79477
80651
  function loadDelegationEmbargo(path = defaultDelegationEmbargoPath()) {
79478
80652
  try {
79479
- const parsed = JSON.parse(readFileSync20(path, "utf8"));
80653
+ const parsed = JSON.parse(readFileSync21(path, "utf8"));
79480
80654
  const entries = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.embargoed) ? parsed.embargoed : [];
79481
80655
  const names = new Set;
79482
80656
  for (const entry2 of entries) {
@@ -79559,15 +80733,15 @@ __export(exports_delegate, {
79559
80733
  registerDelegateCommands: () => registerDelegateCommands
79560
80734
  });
79561
80735
  import chalk12 from "chalk";
79562
- import { readFileSync as readFileSync21 } from "fs";
80736
+ import { readFileSync as readFileSync22 } from "fs";
79563
80737
  function registerDelegateCommands(program2) {
79564
80738
  program2.command("delegate <task> <worker>").description("Hand a filed task to a worker in one call: brief, depth, lineage, assignment, record and notice").option("--brief <path>", "Path to the self-sufficient brief; `-` reads stdin").option("--brief-text <text>", "Inline brief, as an alternative to --brief").option("--depth-threshold <n>", "Open-task count above which this delegation parks").option("--despite-depth", "Proceed past an armed depth threshold; recorded in the [DISPATCH] comment").option("--owner-directive", "Mark as an owner-directive dispatch: depth warns and never parks").option("--seat <slug>", "Seat whose open count is read (default: the lineage parent)").option("--runtime <name>", "Worker runtime label recorded in the comment (e.g. claude-code-subagent)").option("--reports-to <agent>", "Lineage parent for the worker identity (default: the dispatcher)").option("--reuse-identity", "Skip registration and reuse an existing worker identity").option("--depth <n>", "Explicit delegation_depth (default: the task's current depth + 1)").option("--channel <name>", "Channel for the one-line notice (default: $TODOS_DELEGATE_NOTICE_CHANNEL)").option("--no-post", "Skip the channel notice").option("--claim-window <minutes>", `Minutes before the claim deadline (default: ${DEFAULT_CLAIM_WINDOW_MINUTES})`).option("--assign-seat", "Allow <worker> to name a durable seat (a seat queue has no session watching it)").option("--dry-run", "Report all seven effects and perform none").option("-j, --json", "Output as JSON").action(async (taskRef, workerInput, opts) => {
79565
80739
  const globalOpts = program2.opts();
79566
80740
  const useJson = Boolean(opts.json || globalOpts.json);
79567
80741
  try {
79568
80742
  const brief = resolveDelegationBrief({ briefPath: opts.brief, briefText: opts.briefText }, {
79569
- readFile: (path) => readFileSync21(path, "utf8"),
79570
- readStdin: () => readFileSync21(0, "utf8")
80743
+ readFile: (path) => readFileSync22(path, "utf8"),
80744
+ readStdin: () => readFileSync22(0, "utf8")
79571
80745
  });
79572
80746
  if (!brief.ok)
79573
80747
  handleError(new Error(brief.message));
@@ -79815,7 +80989,7 @@ __export(exports_machines, {
79815
80989
  });
79816
80990
  import chalk13 from "chalk";
79817
80991
  import { execSync as execSync4 } from "child_process";
79818
- import { readFileSync as readFileSync22, unlinkSync as unlinkSync3, writeFileSync as writeFileSync14 } from "fs";
80992
+ import { readFileSync as readFileSync23, unlinkSync as unlinkSync3, writeFileSync as writeFileSync14 } from "fs";
79819
80993
  import { tmpdir as tmpdir5 } from "os";
79820
80994
  import { join as join29 } from "path";
79821
80995
  function getOrCreateLocalMachineName() {
@@ -79859,7 +81033,7 @@ function readRemoteBridgeBundle(sshAddress) {
79859
81033
  try {
79860
81034
  runSsh(sshAddress, `todos export --format bridge --allow-plaintext-sensitive --output ${shellQuote(remotePath)}`, 120000);
79861
81035
  scpFromRemote(sshAddress, remotePath, localPath);
79862
- return JSON.parse(readFileSync22(localPath, "utf-8"));
81036
+ return JSON.parse(readFileSync23(localPath, "utf-8"));
79863
81037
  } finally {
79864
81038
  try {
79865
81039
  runSsh(sshAddress, `rm -f ${shellQuote(remotePath)}`, 1e4);
@@ -84817,7 +85991,7 @@ var exports_roadmap_commands = {};
84817
85991
  __export(exports_roadmap_commands, {
84818
85992
  registerRoadmapCommands: () => registerRoadmapCommands
84819
85993
  });
84820
- import { readFileSync as readFileSync23, writeFileSync as writeFileSync16 } from "fs";
85994
+ import { readFileSync as readFileSync24, writeFileSync as writeFileSync16 } from "fs";
84821
85995
  import chalk24 from "chalk";
84822
85996
  function splitList3(value) {
84823
85997
  return value?.split(";").flatMap((part) => part.split(",")).map((item) => item.trim()).filter(Boolean);
@@ -85046,7 +86220,7 @@ function registerRoadmapCommands(program2) {
85046
86220
  const globalOpts = globalOptions(program2);
85047
86221
  try {
85048
86222
  const { importRoadmapBundle: importRoadmapBundle2 } = await Promise.resolve().then(() => (init_roadmaps(), exports_roadmaps));
85049
- const bundle = JSON.parse(readFileSync23(path, "utf8"));
86223
+ const bundle = JSON.parse(readFileSync24(path, "utf8"));
85050
86224
  const result = importRoadmapBundle2(bundle, { apply: Boolean(opts.apply) });
85051
86225
  if (globalOpts.json) {
85052
86226
  output(result, true);
@@ -85806,6 +86980,15 @@ function createShadowTodosStorageAdapter(options) {
85806
86980
  mirror.enqueueUpsert("plans", plan, context);
85807
86981
  return plan;
85808
86982
  },
86983
+ async completeAtRevision(id, expectedUpdatedAt, context) {
86984
+ if (typeof local.plans.completeAtRevision !== "function") {
86985
+ throw new Error("Atomic plan completion is not supported by the local shadow adapter");
86986
+ }
86987
+ const completed = await local.plans.completeAtRevision(id, expectedUpdatedAt, context);
86988
+ if (completed.applied)
86989
+ mirror.enqueueUpsert("plans", completed.plan, context);
86990
+ return completed;
86991
+ },
85809
86992
  async delete(id, context) {
85810
86993
  const deleted = await local.plans.delete(id, context);
85811
86994
  if (deleted)
@@ -87861,6 +89044,7 @@ program2.name("todos").description("Universal task management for AI coding agen
87861
89044
  var authority;
87862
89045
  try {
87863
89046
  authority = initializeTodosCliAuthority();
89047
+ applyTodosCliAuthorityEnvironment(authority);
87864
89048
  } catch (error2) {
87865
89049
  console.error(error2 instanceof Error ? error2.message : String(error2));
87866
89050
  process.exit(1);