@hasna/todos 0.15.17 → 0.15.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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.17",
2126
+ version: "0.15.19",
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",
@@ -2269,7 +2269,7 @@ function isBlockingDependencyStatus(status) {
2269
2269
  function isTerminalStatus(status) {
2270
2270
  return status === "completed" || status === "failed" || status === "cancelled";
2271
2271
  }
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;
2272
+ var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanRevisionConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
2273
2273
  var init_types = __esm(() => {
2274
2274
  TASK_STATUSES = [
2275
2275
  "pending",
@@ -2357,6 +2357,19 @@ var init_types = __esm(() => {
2357
2357
  this.name = "ResourceConflictError";
2358
2358
  }
2359
2359
  };
2360
+ PlanRevisionConflictError = class PlanRevisionConflictError extends Error {
2361
+ planId;
2362
+ expectedUpdatedAt;
2363
+ currentUpdatedAt;
2364
+ static code = "PLAN_REVISION_CONFLICT";
2365
+ constructor(planId, expectedUpdatedAt, currentUpdatedAt) {
2366
+ super(`Plan revision conflict for ${planId}: expected ${expectedUpdatedAt}, current ${currentUpdatedAt}`);
2367
+ this.planId = planId;
2368
+ this.expectedUpdatedAt = expectedUpdatedAt;
2369
+ this.currentUpdatedAt = currentUpdatedAt;
2370
+ this.name = "PlanRevisionConflictError";
2371
+ }
2372
+ };
2360
2373
  PlanNotFoundError = class PlanNotFoundError extends Error {
2361
2374
  planId;
2362
2375
  static code = "PLAN_NOT_FOUND";
@@ -6004,8 +6017,16 @@ async function cloudTaskAction(client, id, action, body = {}) {
6004
6017
  const raw = await client.transport.post(`/tasks/${encodeURIComponent(id)}/${action}`, body);
6005
6018
  return unwrapTask(raw);
6006
6019
  }
6020
+ function isRemoteTaskIdentity(value) {
6021
+ if (!value || typeof value !== "object" || Array.isArray(value))
6022
+ return false;
6023
+ const id = value["id"];
6024
+ return typeof id === "string" && id.length > 0;
6025
+ }
6007
6026
  async function cloudFailTask(client, id, body = {}) {
6008
6027
  const route = `/v1/tasks/${encodeURIComponent(id)}/fail`;
6028
+ if (body.retry === true)
6029
+ await requireRetryCapability(client);
6009
6030
  const raw = await requiredRemoteRoute(client, route, () => client.transport.post(`/tasks/${encodeURIComponent(id)}/fail`, body));
6010
6031
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
6011
6032
  throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} returned an invalid failure response envelope`);
@@ -6016,7 +6037,7 @@ async function cloudFailTask(client, id, body = {}) {
6016
6037
  }
6017
6038
  const task = result["task"];
6018
6039
  const retryTask = result["retryTask"];
6019
- if (!task || typeof task !== "object" || Array.isArray(task) || retryTask !== undefined && (!retryTask || typeof retryTask !== "object" || Array.isArray(retryTask))) {
6040
+ if (!isRemoteTaskIdentity(task) || body.retry === true && !isRemoteTaskIdentity(retryTask) || body.retry !== true && retryTask !== undefined && !isRemoteTaskIdentity(retryTask)) {
6020
6041
  throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} returned an invalid failure result`);
6021
6042
  }
6022
6043
  return result;
@@ -6039,6 +6060,36 @@ function resolveOpenApiSchema(document, schema) {
6039
6060
  }
6040
6061
  return current && typeof current === "object" && !Array.isArray(current) ? current : null;
6041
6062
  }
6063
+ async function fetchRetryCapability(client) {
6064
+ const document = await requiredRemoteRoute(client, "/v1/openapi.json", () => client.transport.get("/openapi.json"));
6065
+ if (!document || typeof document !== "object" || Array.isArray(document))
6066
+ return false;
6067
+ const doc = document;
6068
+ const paths = doc["paths"];
6069
+ const failPath = paths && typeof paths === "object" && !Array.isArray(paths) ? paths["/v1/tasks/{id}/fail"] : undefined;
6070
+ const post = failPath && typeof failPath === "object" && !Array.isArray(failPath) ? failPath["post"] : undefined;
6071
+ const requestBody = post && typeof post === "object" && !Array.isArray(post) ? post["requestBody"] : undefined;
6072
+ const content = requestBody && typeof requestBody === "object" && !Array.isArray(requestBody) ? requestBody["content"] : undefined;
6073
+ const jsonContent = content && typeof content === "object" && !Array.isArray(content) ? content["application/json"] : undefined;
6074
+ const schema = jsonContent && typeof jsonContent === "object" && !Array.isArray(jsonContent) ? jsonContent["schema"] : undefined;
6075
+ const resolved = resolveOpenApiSchema(document, schema);
6076
+ const properties = resolved?.["properties"];
6077
+ if (!properties || typeof properties !== "object" || Array.isArray(properties))
6078
+ return false;
6079
+ const retry = properties["retry"];
6080
+ return !!retry && typeof retry === "object" && !Array.isArray(retry) && retry["type"] === "boolean";
6081
+ }
6082
+ async function requireRetryCapability(client) {
6083
+ const authority = remoteAuthorityBase(client);
6084
+ let capability = retryCapabilityCache.get(authority);
6085
+ if (!capability) {
6086
+ capability = fetchRetryCapability(client);
6087
+ retryCapabilityCache.set(authority, capability);
6088
+ }
6089
+ if (!await capability) {
6090
+ 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");
6091
+ }
6092
+ }
6042
6093
  async function fetchCompletionCapabilities(client) {
6043
6094
  const document = await requiredRemoteRoute(client, "/v1/openapi.json", () => client.transport.get("/openapi.json"));
6044
6095
  if (!document || typeof document !== "object" || Array.isArray(document))
@@ -6296,11 +6347,62 @@ function unwrapPlan(raw) {
6296
6347
  }
6297
6348
  return raw;
6298
6349
  }
6350
+ async function cloudGetPlanById(client, id) {
6351
+ const raw = await client.get("plans", id);
6352
+ return raw ? unwrapPlan(raw) : null;
6353
+ }
6354
+ function isUnavailablePlanPatchRoute(error) {
6355
+ if (!error || typeof error !== "object" || error.status !== 404)
6356
+ return false;
6357
+ const body = error.body;
6358
+ const remoteMessage = body && typeof body === "object" && !Array.isArray(body) ? body.error : undefined;
6359
+ return remoteMessage === "unknown /v1 resource: plans";
6360
+ }
6361
+ function isExactPlanCompletionPatch(patch) {
6362
+ const entries = Object.entries(patch).filter(([, value]) => value !== undefined);
6363
+ return entries.length === 1 && entries[0]?.[0] === "status" && entries[0]?.[1] === "completed";
6364
+ }
6299
6365
  async function cloudCreatePlan(client, input) {
6300
6366
  return unwrapPlan(await requiredRemoteRoute(client, "/v1/plans", () => client.create("plans", input)));
6301
6367
  }
6302
6368
  async function cloudUpdatePlan(client, id, patch) {
6303
- return unwrapPlan(await client.update("plans", id, patch));
6369
+ try {
6370
+ return unwrapPlan(await client.update("plans", id, patch));
6371
+ } catch (error) {
6372
+ if (!isExactPlanCompletionPatch(patch) || !isUnavailablePlanPatchRoute(error))
6373
+ throw error;
6374
+ const existing = await cloudGetPlanById(client, id);
6375
+ if (!existing)
6376
+ throw error;
6377
+ if (existing.status === "completed")
6378
+ return existing;
6379
+ const imported = await requiredRemoteRoute(client, "/v1/import", () => client.transport.post("/import", {
6380
+ source: "postgres",
6381
+ planCompletions: [{
6382
+ id,
6383
+ expected_updated_at: existing.updated_at,
6384
+ status: "completed"
6385
+ }]
6386
+ }));
6387
+ const envelope = imported && typeof imported === "object" && !Array.isArray(imported) ? imported : null;
6388
+ const completion = envelope?.planCompletions?.[0];
6389
+ 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") {
6390
+ throw new Error("REMOTE_API_INCOMPATIBLE: /v1/import did not confirm one atomic plan completion; " + "local SQLite fallback is disabled");
6391
+ }
6392
+ const persisted = await cloudGetPlanById(client, id);
6393
+ if (!persisted) {
6394
+ throw new Error(`REMOTE_API_INCOMPATIBLE: /v1/import acknowledged plan completion ${id} but authoritative readback returned no plan; ` + "local SQLite fallback is disabled");
6395
+ }
6396
+ if (persisted.status !== "completed" || persisted.updated_at !== completion.result_updated_at) {
6397
+ throw new Error(`REMOTE_API_INCOMPATIBLE: /v1/import completion readback did not match its receipt for plan ${id}; ` + "local SQLite fallback is disabled");
6398
+ }
6399
+ for (const key of PLAN_COMPLETION_PROTECTED_FIELDS) {
6400
+ if (persisted[key] !== existing[key]) {
6401
+ throw new Error(`REMOTE_PLAN_COMPLETION_CONFLICT: /v1/import completion changed protected plan field ${key}; ` + "local SQLite fallback is disabled");
6402
+ }
6403
+ }
6404
+ return persisted;
6405
+ }
6304
6406
  }
6305
6407
  async function cloudDeletePlan(client, id) {
6306
6408
  try {
@@ -6840,7 +6942,7 @@ function resolveTaskListFromCandidates(lists, input) {
6840
6942
  ];
6841
6943
  for (const matches of matchGroups) {
6842
6944
  if (matches.length === 1)
6843
- return matches[0].id;
6945
+ return matches[0];
6844
6946
  if (matches.length > 1) {
6845
6947
  throw new Error(`Task list reference is ambiguous: "${input}"`);
6846
6948
  }
@@ -6853,27 +6955,26 @@ async function legacyProjectTaskLists(client, projectId) {
6853
6955
  return [];
6854
6956
  return (await cloudListTaskLists(client)).filter((list) => list.project_id == null && list.slug === project.task_list_id);
6855
6957
  }
6856
- async function cloudResolveTaskListRef(client, ref, projectId) {
6958
+ async function cloudResolveTaskList(client, ref, projectId) {
6857
6959
  const input = ref.trim();
6858
6960
  const normalizedIdRef = input.toLowerCase();
6859
- if (UUID_RE.test(input) && !projectId)
6860
- return normalizedIdRef;
6861
- if (UUID_RE.test(input) && projectId) {
6961
+ if (UUID_RE.test(input)) {
6862
6962
  const direct = await cloudGetTaskList(client, normalizedIdRef);
6863
6963
  if (direct?.id?.toLowerCase() === normalizedIdRef) {
6864
- if (direct.project_id === projectId)
6865
- return direct.id;
6964
+ if (!projectId || direct.project_id === projectId)
6965
+ return direct;
6866
6966
  if (direct.project_id == null) {
6867
6967
  const project = await cloudGetProjectById(client, projectId);
6868
6968
  if (project?.task_list_id === direct.slug)
6869
- return direct.id;
6969
+ return direct;
6870
6970
  }
6871
6971
  throw new Error(`Task list not found: "${input}"`);
6872
6972
  }
6873
6973
  }
6874
6974
  const scopedMatch = resolveTaskListFromCandidates(await cloudListTaskLists(client, projectId), input);
6875
- if (scopedMatch)
6975
+ if (scopedMatch && (!projectId || scopedMatch.project_id === projectId)) {
6876
6976
  return scopedMatch;
6977
+ }
6877
6978
  if (projectId) {
6878
6979
  const legacyMatch = resolveTaskListFromCandidates(await legacyProjectTaskLists(client, projectId), input);
6879
6980
  if (legacyMatch)
@@ -6881,6 +6982,12 @@ async function cloudResolveTaskListRef(client, ref, projectId) {
6881
6982
  }
6882
6983
  throw new Error(`Task list not found: "${input}"`);
6883
6984
  }
6985
+ async function cloudResolveTaskListRef(client, ref, projectId) {
6986
+ const input = ref.trim();
6987
+ if (UUID_RE.test(input) && !projectId)
6988
+ return input.toLowerCase();
6989
+ return (await cloudResolveTaskList(client, ref, projectId)).id;
6990
+ }
6884
6991
  async function cloudCreateTaskList(client, input) {
6885
6992
  return unwrapTaskList(await requiredRemoteRoute(client, "/v1/task-lists", () => client.transport.post("/task-lists", input)));
6886
6993
  }
@@ -7038,7 +7145,7 @@ async function cloudTimeline(client, options = {}) {
7038
7145
  const limit = options.limit ?? 50;
7039
7146
  return { entries: entries.slice(offset, offset + limit), total, limit, offset };
7040
7147
  }
7041
- var UUID_RE, TRANSPORT_TOKENS, COMPLETION_EVIDENCE_FIELDS, completionCapabilityCache, gitRefCapabilityCache, SERVER_MODE_CANDIDATES, cachedServerMode = null, PRIORITY_RANK, listTagsCapabilityCache, RELATION_HYDRATION_CONCURRENCY = 6;
7148
+ 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;
7042
7149
  var init_cloud_router = __esm(() => {
7043
7150
  init_types();
7044
7151
  init_redaction();
@@ -7063,10 +7170,23 @@ var init_cloud_router = __esm(() => {
7063
7170
  "confidence"
7064
7171
  ];
7065
7172
  completionCapabilityCache = new Map;
7173
+ retryCapabilityCache = new Map;
7066
7174
  gitRefCapabilityCache = new Map;
7067
7175
  SERVER_MODE_CANDIDATES = ["postgres", "cloud", "self_hosted"];
7068
7176
  PRIORITY_RANK = { critical: 0, high: 1, medium: 2, low: 3 };
7069
7177
  listTagsCapabilityCache = new Map;
7178
+ PLAN_COMPLETION_PROTECTED_FIELDS = [
7179
+ "id",
7180
+ "slug",
7181
+ "project_id",
7182
+ "task_list_id",
7183
+ "agent_id",
7184
+ "name",
7185
+ "description",
7186
+ "created_at",
7187
+ "machine_id",
7188
+ "synced_at"
7189
+ ];
7070
7190
  });
7071
7191
 
7072
7192
  // src/cli/stage-a.ts
@@ -18328,6 +18448,48 @@ function updatePlan(id, input, db) {
18328
18448
  return updatePlanStored(id, input, d);
18329
18449
  })();
18330
18450
  }
18451
+ function nextPlanCompletionTimestamp(expectedUpdatedAt) {
18452
+ const expected = Date.parse(expectedUpdatedAt);
18453
+ const minimum = Number.isNaN(expected) ? Date.now() : expected + 2;
18454
+ return new Date(Math.max(Date.now(), minimum)).toISOString();
18455
+ }
18456
+ function completePlanAtRevision(id, expectedUpdatedAt, db) {
18457
+ const d = db || getDatabase();
18458
+ return d.transaction(() => {
18459
+ guardPlanRowsSqlite([id], d);
18460
+ const plan = getPlan(id, d);
18461
+ if (!plan)
18462
+ throw new PlanNotFoundError(id);
18463
+ if (plan.updated_at !== expectedUpdatedAt) {
18464
+ throw new PlanRevisionConflictError(id, expectedUpdatedAt, plan.updated_at);
18465
+ }
18466
+ if (plan.status === "completed")
18467
+ return { plan, applied: false };
18468
+ const updatedAt = nextPlanCompletionTimestamp(expectedUpdatedAt);
18469
+ const result = d.run(`UPDATE plans
18470
+ SET status = 'completed', updated_at = ?
18471
+ WHERE id = ? AND updated_at = ? AND status <> 'completed'`, [updatedAt, id, expectedUpdatedAt]);
18472
+ if (result.changes !== 1) {
18473
+ const current = getPlan(id, d);
18474
+ if (!current)
18475
+ throw new PlanNotFoundError(id);
18476
+ throw new PlanRevisionConflictError(id, expectedUpdatedAt, current.updated_at);
18477
+ }
18478
+ const completed = getPlan(id, d);
18479
+ emitLocalEventHooksQuiet({
18480
+ type: "plan.updated",
18481
+ payload: {
18482
+ id,
18483
+ old_status: plan.status,
18484
+ new_status: completed.status,
18485
+ name: completed.name,
18486
+ project_id: completed.project_id
18487
+ },
18488
+ databasePath: databasePathFromDatabase(d)
18489
+ });
18490
+ return { plan: completed, applied: true };
18491
+ })();
18492
+ }
18331
18493
  function deletePlan(id, db) {
18332
18494
  const d = db || getDatabase();
18333
18495
  const plan = getPlan(id, d);
@@ -22125,7 +22287,11 @@ function registerTaskCommands(program2) {
22125
22287
  filter["project_id"] = projectId;
22126
22288
  }
22127
22289
  if (opts.list && cloud) {
22128
- filter["task_list_id"] = await cloudResolveTaskListRef(cloud, opts.list, projectId);
22290
+ const resolvedTaskList = await cloudResolveTaskList(cloud, opts.list, projectId);
22291
+ filter["task_list_id"] = resolvedTaskList.id;
22292
+ if (!projectId && resolvedTaskList.project_id) {
22293
+ filter["project_id"] = resolvedTaskList.project_id;
22294
+ }
22129
22295
  } else if (opts.list) {
22130
22296
  const db = getDatabase();
22131
22297
  const listId = resolvePartialId(db, "task_lists", opts.list);
@@ -22193,7 +22359,8 @@ function registerTaskCommands(program2) {
22193
22359
  const requestedLimit = filter["limit"];
22194
22360
  const combinesScalarStatusPages = Boolean(cloud && Array.isArray(filter["status"]) && filter["status"].length > 1);
22195
22361
  const reordersAfterQuery = Boolean(opts.sort) || combinesScalarStatusPages;
22196
- const narrowsAfterQuery = Boolean(opts.dueToday) || Boolean(opts.overdue) || creatorFilterActive && cloud;
22362
+ const taskListFilterActive = Boolean(cloud && filter["task_list_id"]);
22363
+ const narrowsAfterQuery = Boolean(opts.dueToday) || Boolean(opts.overdue) || creatorFilterActive && cloud || taskListFilterActive;
22197
22364
  const withholdLimit = requestedLimit !== undefined && (reordersAfterQuery || narrowsAfterQuery);
22198
22365
  const scanCeiling = cloud && (withholdLimit || requestedLimit === undefined) ? Math.max(requestedLimit ?? 0, listScanLimit()) : undefined;
22199
22366
  const serverFilter = (() => {
@@ -24223,7 +24390,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
24223
24390
  }
24224
24391
  if (result.errors.length > 0)
24225
24392
  return result;
24226
- const applyRows = (objectType, table, columns, rows, updateClockColumn, afterUpsert) => {
24393
+ const applyRows = (objectType, table, columns, rows, updateClockColumn, acceptEqualClock = true, afterUpsert) => {
24227
24394
  for (const row of rows) {
24228
24395
  try {
24229
24396
  const record = asRecord(row);
@@ -24232,7 +24399,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
24232
24399
  result.skipped += 1;
24233
24400
  continue;
24234
24401
  }
24235
- const state = upsertById(d, table, columns, record, updateClockColumn);
24402
+ const state = upsertById(d, table, columns, record, updateClockColumn, acceptEqualClock);
24236
24403
  if (state === "inserted")
24237
24404
  result.inserted += 1;
24238
24405
  else if (state === "updated")
@@ -24249,10 +24416,10 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
24249
24416
  applyRows("project_machine_paths", "project_machine_paths", PROJECT_MACHINE_PATH_COLUMNS, snapshot.projectMachinePaths ?? [], "updated_at");
24250
24417
  applyRows("agents", "agents", AGENT_COLUMNS, snapshot.agents, "last_seen_at");
24251
24418
  applyRows("task_lists", "task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
24252
- applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at");
24419
+ applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at", false);
24253
24420
  applyRows("templates", "task_templates", TEMPLATE_COLUMNS, snapshot.templates);
24254
24421
  applyRows("template_tasks", "template_tasks", TEMPLATE_TASK_COLUMNS, snapshot.templateTasks ?? []);
24255
- applyRows("tasks", "tasks", TASK_COLUMNS, sortedTasks(snapshot.tasks), "updated_at", (row, changed) => {
24422
+ applyRows("tasks", "tasks", TASK_COLUMNS, sortedTasks(snapshot.tasks), "updated_at", true, (row, changed) => {
24256
24423
  if (changed && Array.isArray(row["tags"]) && typeof row["id"] === "string") {
24257
24424
  replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
24258
24425
  }
@@ -24261,7 +24428,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
24261
24428
  applyTombstones(d, snapshot.tombstones ?? [], result);
24262
24429
  return result;
24263
24430
  }
24264
- function upsertById(db, table, columns, row, updateClockColumn) {
24431
+ function upsertById(db, table, columns, row, updateClockColumn, acceptEqualClock = true) {
24265
24432
  const id = row["id"];
24266
24433
  if (typeof id !== "string" || !id)
24267
24434
  throw new Error(`${table} row is missing id`);
@@ -24273,7 +24440,7 @@ function upsertById(db, table, columns, row, updateClockColumn) {
24273
24440
  const values = presentColumns.map((column) => valueForColumn(column, row[column]));
24274
24441
  const updateColumns = presentColumns.filter((column) => column !== "id");
24275
24442
  const updateSet = updateColumns.map((column) => column === "version" ? `version = MAX(COALESCE(${table}.version, 0), excluded.version)` : `${column} = excluded.${column}`).join(", ");
24276
- const clockGuard = updateClockColumn && presentColumns.includes(updateClockColumn) ? ` WHERE ${table}.${updateClockColumn} IS NULL OR ${table}.${updateClockColumn} <= excluded.${updateClockColumn}` : "";
24443
+ const clockGuard = updateClockColumn && presentColumns.includes(updateClockColumn) ? ` WHERE ${table}.${updateClockColumn} IS NULL OR ${table}.${updateClockColumn} ${acceptEqualClock ? "<=" : "<"} excluded.${updateClockColumn}` : "";
24277
24444
  const sql = updateSet ? `INSERT INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders})
24278
24445
  ON CONFLICT(id) DO UPDATE SET ${updateSet}${clockGuard}` : `INSERT OR IGNORE INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders})`;
24279
24446
  const changes = db.run(sql, values).changes;
@@ -24692,6 +24859,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
24692
24859
  get: (id) => getPlan(id, database()),
24693
24860
  list: (projectId) => listPlans(projectId, database()),
24694
24861
  update: (id, input) => updatePlan(id, input, database()),
24862
+ completeAtRevision: (id, expectedUpdatedAt) => completePlanAtRevision(id, expectedUpdatedAt, database()),
24695
24863
  delete: (id) => deletePlan(id, database())
24696
24864
  },
24697
24865
  planProjectLinks: {
@@ -29224,6 +29392,7 @@ __export(exports_project_commands, {
29224
29392
  registerProjectCommands: () => registerProjectCommands
29225
29393
  });
29226
29394
  import chalk5 from "chalk";
29395
+ import { readFileSync as readFileSync9, statSync as statSync6 } from "fs";
29227
29396
  import { basename as basename5, resolve as resolve13, sep as sep3 } from "path";
29228
29397
  function collectOption(value, previous = []) {
29229
29398
  return [...previous, value];
@@ -29417,18 +29586,45 @@ function registerProjectCommands(program2) {
29417
29586
  handleError(e);
29418
29587
  }
29419
29588
  });
29420
- 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) => {
29589
+ 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) => {
29590
+ const hasPositionalContent = text !== undefined;
29591
+ const hasFileContent = opts.file !== undefined;
29592
+ if (hasPositionalContent === hasFileContent) {
29593
+ handleError(new Error("Provide exactly one comment content source: positional text or --file <path>, not both."));
29594
+ }
29595
+ let content;
29596
+ if (opts.file !== undefined) {
29597
+ const commentFilePath = resolve13(opts.file);
29598
+ let isRegularFile = false;
29599
+ try {
29600
+ isRegularFile = statSync6(commentFilePath).isFile();
29601
+ } catch (error) {
29602
+ handleError(new Error(`Unable to read comment file "${opts.file}".`, { cause: error }));
29603
+ }
29604
+ if (!isRegularFile) {
29605
+ handleError(new Error(`Comment file "${opts.file}" must be a regular file.`));
29606
+ }
29607
+ try {
29608
+ content = readFileSync9(commentFilePath, "utf8");
29609
+ } catch (error) {
29610
+ handleError(new Error(`Unable to read comment file "${opts.file}".`, { cause: error }));
29611
+ }
29612
+ } else {
29613
+ content = text;
29614
+ }
29615
+ if (!content.trim()) {
29616
+ handleError(new Error("Comment content must not be empty."));
29617
+ }
29421
29618
  const globalOpts = program2.opts();
29422
29619
  const cloud = getTodosCloudClient();
29423
29620
  const resolvedId = await resolveTaskIdForCommand(id, cloud);
29424
- let content = text;
29425
29621
  let progressPct;
29426
29622
  if (opts.pct !== undefined) {
29427
29623
  const pct = parseInt(opts.pct, 10);
29428
29624
  if (!Number.isFinite(pct) || pct < 0 || pct > 100) {
29429
29625
  handleError(new Error("--pct must be a number between 0 and 100"));
29430
29626
  }
29431
- content = `[progress ${pct}%] ${text}`;
29627
+ content = `[progress ${pct}%] ${content}`;
29432
29628
  progressPct = pct;
29433
29629
  }
29434
29630
  const router = resolveWritableIdentity(globalOpts.agent);
@@ -30188,10 +30384,10 @@ Indexed ${result.index.files.length} file(s), ${result.index.total_symbols} symb
30188
30384
  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) => {
30189
30385
  const globalOpts = program2.opts();
30190
30386
  try {
30191
- const { readFileSync: readFileSync9 } = await import("fs");
30387
+ const { readFileSync: readFileSync10 } = await import("fs");
30192
30388
  const { importLocalBridgeBundle: importLocalBridgeBundle2 } = await Promise.resolve().then(() => (init_local_bridge(), exports_local_bridge));
30193
30389
  const { decryptBridgeBundle: decryptBridgeBundle2, isEncryptedBridgeBundle: isEncryptedBridgeBundle2 } = await Promise.resolve().then(() => (init_local_encryption(), exports_local_encryption));
30194
- const parsed = JSON.parse(readFileSync9(resolve13(file), "utf-8"));
30390
+ const parsed = JSON.parse(readFileSync10(resolve13(file), "utf-8"));
30195
30391
  const bundle = isEncryptedBridgeBundle2(parsed) ? opts.decrypt ? decryptBridgeBundle2(parsed) : (() => {
30196
30392
  throw new Error("Bridge bundle is encrypted. Re-run with --decrypt and the configured key environment variable set.");
30197
30393
  })() : parsed;
@@ -30225,9 +30421,9 @@ Indexed ${result.index.files.length} file(s), ${result.index.total_symbols} symb
30225
30421
  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) => {
30226
30422
  const globalOpts = program2.opts();
30227
30423
  try {
30228
- const { readFileSync: readFileSync9 } = await import("fs");
30424
+ const { readFileSync: readFileSync10 } = await import("fs");
30229
30425
  const { importTodosMarkdown: importTodosMarkdown2 } = await Promise.resolve().then(() => (init_todos_md(), exports_todos_md));
30230
- const result = importTodosMarkdown2(readFileSync9(resolve13(file), "utf-8"), { dryRun: !opts.apply, conflictStrategy: opts.resolveConflicts ? "safe_merge" : "skip" });
30426
+ const result = importTodosMarkdown2(readFileSync10(resolve13(file), "utf-8"), { dryRun: !opts.apply, conflictStrategy: opts.resolveConflicts ? "safe_merge" : "skip" });
30231
30427
  const { emitLocalEventHooksQuiet: emitLocalEventHooksQuiet2 } = await Promise.resolve().then(() => (init_event_hooks(), exports_event_hooks));
30232
30428
  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 } });
30233
30429
  if (globalOpts.json) {
@@ -30874,7 +31070,7 @@ async function findFreePort(start) {
30874
31070
  var DEFAULT_PORT = 19427;
30875
31071
 
30876
31072
  // src/lib/db-backup.ts
30877
- import { existsSync as existsSync15, copyFileSync, mkdirSync as mkdirSync8, readFileSync as readFileSync9, renameSync, statSync as statSync6, writeFileSync as writeFileSync7, unlinkSync } from "fs";
31073
+ import { existsSync as existsSync15, copyFileSync, mkdirSync as mkdirSync8, readFileSync as readFileSync10, renameSync, statSync as statSync7, writeFileSync as writeFileSync7, unlinkSync } from "fs";
30878
31074
  import { dirname as dirname7, join as join14, resolve as resolve14 } from "path";
30879
31075
  import { Database as Database2 } from "bun:sqlite";
30880
31076
  function resolveDbPath(dbPath) {
@@ -30903,7 +31099,7 @@ function backupDatabase(outputPath, sourcePath) {
30903
31099
  src.close();
30904
31100
  writeFileSync7(outputPath, image);
30905
31101
  const method = "file_copy";
30906
- const bytes = statSync6(outputPath).size;
31102
+ const bytes = statSync7(outputPath).size;
30907
31103
  return {
30908
31104
  schema_version: DB_BACKUP_SCHEMA,
30909
31105
  source_path: source,
@@ -32240,7 +32436,7 @@ __export(exports_local_extensions, {
32240
32436
  discoverLocalExtensions: () => discoverLocalExtensions
32241
32437
  });
32242
32438
  import { createHash as createHash9, createVerify } from "crypto";
32243
- import { existsSync as existsSync17, readdirSync as readdirSync3, readFileSync as readFileSync10, statSync as statSync7 } from "fs";
32439
+ import { existsSync as existsSync17, readdirSync as readdirSync3, readFileSync as readFileSync11, statSync as statSync8 } from "fs";
32244
32440
  import { basename as basename6, join as join16, resolve as resolve15 } from "path";
32245
32441
  function isObject(value) {
32246
32442
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
@@ -32322,7 +32518,7 @@ function normalizeManifest(input) {
32322
32518
  };
32323
32519
  }
32324
32520
  function parseJson(path) {
32325
- return JSON.parse(readFileSync10(path, "utf8"));
32521
+ return JSON.parse(readFileSync11(path, "utf8"));
32326
32522
  }
32327
32523
  function sha2564(bytes) {
32328
32524
  return `sha256:${createHash9("sha256").update(bytes).digest("hex")}`;
@@ -32502,11 +32698,11 @@ function inspectExtensionSource(source2) {
32502
32698
  const resolved = resolve15(source2);
32503
32699
  if (!existsSync17(resolved))
32504
32700
  throw new Error(`extension source not found: ${source2}`);
32505
- const stat = statSync7(resolved);
32701
+ const stat = statSync8(resolved);
32506
32702
  const manifestPath = stat.isDirectory() ? [join16(resolved, "todos.extension.json"), join16(resolved, "extension.json")].find(existsSync17) : resolved;
32507
32703
  if (!manifestPath)
32508
32704
  throw new Error(`extension directory ${source2} is missing todos.extension.json`);
32509
- const raw = readFileSync10(manifestPath);
32705
+ const raw = readFileSync11(manifestPath);
32510
32706
  const parsed = parseJson(manifestPath);
32511
32707
  const bundle = isObject(parsed) && isObject(parsed["manifest"]);
32512
32708
  const manifest = normalizeManifest(bundle ? parsed["manifest"] : parsed);
@@ -32608,7 +32804,7 @@ function projectExtensionSources(projectPath) {
32608
32804
  if (entry.startsWith("."))
32609
32805
  continue;
32610
32806
  const full = join16(extensionDir, entry);
32611
- if (statSync7(full).isDirectory() || entry.endsWith(".json"))
32807
+ if (statSync8(full).isDirectory() || entry.endsWith(".json"))
32612
32808
  candidates.push(full);
32613
32809
  }
32614
32810
  }
@@ -34313,6 +34509,7 @@ function createPostgresTodosStorageAdapter(options) {
34313
34509
  get: (id) => store.get("plans", id),
34314
34510
  list: async (projectId) => (await store.list("plans")).filter((plan) => projectId === undefined || plan.project_id === projectId).sort((a, b) => a.name.localeCompare(b.name)),
34315
34511
  update: (id, input) => updatePlan2(id, input, store),
34512
+ completeAtRevision: (id, expectedUpdatedAt, context) => store.completePlanAtRevision(id, expectedUpdatedAt, context),
34316
34513
  delete: (id, context) => store.deletePlan(id, context)
34317
34514
  },
34318
34515
  planProjectLinks: {
@@ -34800,6 +34997,54 @@ class PostgresJsonRecordStore {
34800
34997
  throw new PlanNotFoundError(value.id);
34801
34998
  return payloadRecord2(row.payload);
34802
34999
  }
35000
+ async completePlanAtRevision(id, expectedUpdatedAt, context = {}) {
35001
+ await this.ensureSchema();
35002
+ const result = await this.options.client.query(`/* todos:complete-plan-revision-cas */ WITH next_clock AS (
35003
+ SELECT date_trunc(
35004
+ 'milliseconds',
35005
+ GREATEST(clock_timestamp(), ($3::text)::timestamptz + interval '2 milliseconds')
35006
+ ) AS completed_at
35007
+ ), stored AS (
35008
+ UPDATE ${this.tableName} AS record SET
35009
+ payload = record.payload || jsonb_build_object(
35010
+ 'status', 'completed',
35011
+ 'updated_at', to_char(
35012
+ next_clock.completed_at AT TIME ZONE 'UTC',
35013
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
35014
+ )
35015
+ ),
35016
+ updated_at = next_clock.completed_at,
35017
+ deleted_at = NULL,
35018
+ source_machine_id = COALESCE($4, record.source_machine_id),
35019
+ version = COALESCE(record.version, 0) + 1
35020
+ FROM next_clock
35021
+ WHERE record.service = $1
35022
+ AND record.object_type = 'plans'
35023
+ AND record.object_id = $2
35024
+ AND record.deleted_at IS NULL
35025
+ AND record.payload->>'updated_at' = $3::text
35026
+ AND record.payload->>'status' IS DISTINCT FROM 'completed'
35027
+ RETURNING record.payload
35028
+ )
35029
+ SELECT payload FROM stored`, [
35030
+ this.service,
35031
+ id,
35032
+ expectedUpdatedAt,
35033
+ context.requestId ?? this.sourceMachineId ?? null
35034
+ ]);
35035
+ const payload = result.rows[0]?.payload;
35036
+ if (payload)
35037
+ return { plan: payloadRecord2(payload), applied: true };
35038
+ const current = await this.get("plans", id);
35039
+ if (!current)
35040
+ throw new PlanNotFoundError(id);
35041
+ if (current.updated_at !== expectedUpdatedAt) {
35042
+ throw new PlanRevisionConflictError(id, expectedUpdatedAt, current.updated_at);
35043
+ }
35044
+ if (current.status === "completed")
35045
+ return { plan: current, applied: false };
35046
+ throw new PlanRevisionConflictError(id, expectedUpdatedAt, current.updated_at);
35047
+ }
34803
35048
  async createTemplateWithTasks(template, tasks, context = {}) {
34804
35049
  await this.ensureSchema();
34805
35050
  const records = [
@@ -44635,7 +44880,7 @@ var exports_doctor = {};
44635
44880
  __export(exports_doctor, {
44636
44881
  runTodosDoctor: () => runTodosDoctor
44637
44882
  });
44638
- import { chmodSync as chmodSync2, copyFileSync as copyFileSync2, existsSync as existsSync18, mkdirSync as mkdirSync9, statSync as statSync8 } from "fs";
44883
+ import { chmodSync as chmodSync2, copyFileSync as copyFileSync2, existsSync as existsSync18, mkdirSync as mkdirSync9, statSync as statSync9 } from "fs";
44639
44884
  import { basename as basename7, dirname as dirname8, join as join17 } from "path";
44640
44885
  function tableExists3(db, table) {
44641
44886
  return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
@@ -44782,7 +45027,7 @@ function databasePermissionsAreUnsafe(dbPath) {
44782
45027
  if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
44783
45028
  return false;
44784
45029
  try {
44785
- return (statSync8(dbPath).mode & 63) !== 0;
45030
+ return (statSync9(dbPath).mode & 63) !== 0;
44786
45031
  } catch {
44787
45032
  return false;
44788
45033
  }
@@ -48000,8 +48245,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
48000
48245
  "/v1/import": {
48001
48246
  post: {
48002
48247
  operationId: "importSnapshot",
48003
- summary: "Bulk-ingest a full or partial snapshot (idempotent upsert by id)",
48004
- 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.",
48248
+ summary: "Bulk-ingest a snapshot or atomically complete one observed plan",
48249
+ 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.",
48005
48250
  requestBody: {
48006
48251
  required: true,
48007
48252
  content: {
@@ -48020,7 +48265,22 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
48020
48265
  templates: { type: "array", items: { type: "object" } },
48021
48266
  templateTasks: { type: "array", items: { $ref: "#/components/schemas/TemplateTask" } },
48022
48267
  auditHistory: { type: "array", items: { type: "object" } },
48023
- tombstones: { type: "array", items: { type: "object" } }
48268
+ tombstones: { type: "array", items: { type: "object" } },
48269
+ planCompletions: {
48270
+ type: "array",
48271
+ minItems: 1,
48272
+ maxItems: 1,
48273
+ items: {
48274
+ type: "object",
48275
+ additionalProperties: false,
48276
+ required: ["id", "expected_updated_at", "status"],
48277
+ properties: {
48278
+ id: { type: "string" },
48279
+ expected_updated_at: { type: "string", format: "date-time" },
48280
+ status: { type: "string", enum: ["completed"] }
48281
+ }
48282
+ }
48283
+ }
48024
48284
  }
48025
48285
  }
48026
48286
  }
@@ -48043,6 +48303,26 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
48043
48303
  skipped: { type: "number" },
48044
48304
  errors: { type: "array", items: { type: "string" } }
48045
48305
  }
48306
+ },
48307
+ planCompletions: {
48308
+ type: "array",
48309
+ items: {
48310
+ type: "object",
48311
+ required: [
48312
+ "id",
48313
+ "status",
48314
+ "expected_updated_at",
48315
+ "result_updated_at",
48316
+ "applied"
48317
+ ],
48318
+ properties: {
48319
+ id: { type: "string" },
48320
+ status: { type: "string", enum: ["completed"] },
48321
+ expected_updated_at: { type: "string", format: "date-time" },
48322
+ result_updated_at: { type: "string", format: "date-time" },
48323
+ applied: { type: "boolean" }
48324
+ }
48325
+ }
48046
48326
  }
48047
48327
  }
48048
48328
  }
@@ -48892,6 +49172,65 @@ function normalizeImportSnapshot(raw) {
48892
49172
  function countSnapshotRecords(s) {
48893
49173
  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);
48894
49174
  }
49175
+ function validatePlanCompletionImports(raw) {
49176
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
49177
+ return { present: false, operations: [] };
49178
+ }
49179
+ const body2 = raw;
49180
+ if (!Object.prototype.hasOwnProperty.call(body2, "planCompletions")) {
49181
+ return { present: false, operations: [] };
49182
+ }
49183
+ if (!Array.isArray(body2["planCompletions"]) || body2["planCompletions"].length !== 1) {
49184
+ return {
49185
+ present: true,
49186
+ operations: [],
49187
+ error: "planCompletions must contain exactly one completion operation"
49188
+ };
49189
+ }
49190
+ const operation = body2["planCompletions"][0];
49191
+ if (!operation || typeof operation !== "object" || Array.isArray(operation)) {
49192
+ return { present: true, operations: [], error: "plan completion must be an object" };
49193
+ }
49194
+ const record = operation;
49195
+ const allowed = new Set(["id", "expected_updated_at", "status"]);
49196
+ const unknown = Object.keys(record).find((key2) => !allowed.has(key2));
49197
+ if (unknown) {
49198
+ return { present: true, operations: [], error: `unknown plan completion field: ${unknown}` };
49199
+ }
49200
+ if (typeof record["id"] !== "string" || !record["id"].trim()) {
49201
+ return { present: true, operations: [], error: "plan completion id must be a non-empty string" };
49202
+ }
49203
+ if (record["status"] !== "completed") {
49204
+ return { present: true, operations: [], error: "plan completion status must be completed" };
49205
+ }
49206
+ const expectedUpdatedAt = typeof record["expected_updated_at"] === "string" ? record["expected_updated_at"] : "";
49207
+ const timestampMatch = RFC3339_DATE_TIME.exec(expectedUpdatedAt);
49208
+ const parsedTimestamp = Date.parse(expectedUpdatedAt);
49209
+ if (!timestampMatch || Number.isNaN(parsedTimestamp)) {
49210
+ return {
49211
+ present: true,
49212
+ operations: [],
49213
+ error: "plan completion expected_updated_at must be an RFC 3339 date-time with an explicit offset"
49214
+ };
49215
+ }
49216
+ const [, year, month, day] = timestampMatch;
49217
+ const calendarProbe = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
49218
+ if (calendarProbe.getUTCFullYear() !== Number(year) || calendarProbe.getUTCMonth() !== Number(month) - 1 || calendarProbe.getUTCDate() !== Number(day)) {
49219
+ return {
49220
+ present: true,
49221
+ operations: [],
49222
+ error: "plan completion expected_updated_at names a date that does not exist"
49223
+ };
49224
+ }
49225
+ return {
49226
+ present: true,
49227
+ operations: [{
49228
+ id: record["id"],
49229
+ expected_updated_at: expectedUpdatedAt,
49230
+ status: "completed"
49231
+ }]
49232
+ };
49233
+ }
48895
49234
  async function handleV1Request(req, url, dependencies = {}) {
48896
49235
  const path = url.pathname;
48897
49236
  if (path !== "/v1" && !path.startsWith("/v1/"))
@@ -49828,6 +50167,36 @@ async function handleV1Request(req, url, dependencies = {}) {
49828
50167
  return error(400, "invalid JSON body");
49829
50168
  const snapshot = normalizeImportSnapshot(raw);
49830
50169
  const received = countSnapshotRecords(snapshot);
50170
+ const completionImports = validatePlanCompletionImports(raw);
50171
+ if (completionImports.present) {
50172
+ if (completionImports.error)
50173
+ return error(400, completionImports.error);
50174
+ if (received !== 0) {
50175
+ return error(400, "planCompletions cannot be combined with snapshot record arrays");
50176
+ }
50177
+ if (typeof store.plans.completeAtRevision !== "function") {
50178
+ return error(501, "atomic plan completion is not supported by this storage backend");
50179
+ }
50180
+ const operation = completionImports.operations[0];
50181
+ const completed = await store.plans.completeAtRevision(operation.id, operation.expected_updated_at, contextFromPrincipal(principal));
50182
+ return json5({
50183
+ result: {
50184
+ inserted: 0,
50185
+ updated: completed.applied ? 1 : 0,
50186
+ deleted: 0,
50187
+ skipped: completed.applied ? 0 : 1,
50188
+ errors: []
50189
+ },
50190
+ received: 1,
50191
+ planCompletions: [{
50192
+ id: operation.id,
50193
+ status: "completed",
50194
+ expected_updated_at: operation.expected_updated_at,
50195
+ result_updated_at: completed.plan.updated_at,
50196
+ applied: completed.applied
50197
+ }]
50198
+ });
50199
+ }
49831
50200
  if (received === 0) {
49832
50201
  return error(400, "empty snapshot: provide at least one record array (tasks/projects/plans/...)");
49833
50202
  }
@@ -49854,6 +50223,18 @@ async function handleV1Request(req, url, dependencies = {}) {
49854
50223
  if (e instanceof TaskNotFoundError) {
49855
50224
  return error(404, e.message, { code: TaskNotFoundError.code });
49856
50225
  }
50226
+ if (e instanceof PlanNotFoundError) {
50227
+ return error(404, e.message, { code: PlanNotFoundError.code });
50228
+ }
50229
+ if (e instanceof PlanRevisionConflictError) {
50230
+ return error(409, e.message, {
50231
+ code: PlanRevisionConflictError.code,
50232
+ conflict: true,
50233
+ plan_id: e.planId,
50234
+ expected_updated_at: e.expectedUpdatedAt,
50235
+ current_updated_at: e.currentUpdatedAt
50236
+ });
50237
+ }
49857
50238
  if (e instanceof LockError)
49858
50239
  return error(409, e.message, { code: LockError.code });
49859
50240
  if (e instanceof TaskNotStartableError) {
@@ -51005,7 +51386,7 @@ var exports_mention_resolver = {};
51005
51386
  __export(exports_mention_resolver, {
51006
51387
  resolveMentions: () => resolveMentions
51007
51388
  });
51008
- import { existsSync as existsSync19, readdirSync as readdirSync4, readFileSync as readFileSync11, statSync as statSync9 } from "fs";
51389
+ import { existsSync as existsSync19, readdirSync as readdirSync4, readFileSync as readFileSync12, statSync as statSync10 } from "fs";
51009
51390
  import { basename as basename8, isAbsolute, join as join19, relative as relative5, resolve as resolve18, sep as sep5 } from "path";
51010
51391
  function blankResolution(parsed) {
51011
51392
  return {
@@ -51108,13 +51489,13 @@ function resolveFile(parsed, workspace) {
51108
51489
  resolution.warnings.push("file does not exist in the local workspace");
51109
51490
  return resolution;
51110
51491
  }
51111
- const stats = statSync9(absolutePath);
51492
+ const stats = statSync10(absolutePath);
51112
51493
  if (!stats.isFile()) {
51113
51494
  resolution.warnings.push("path exists but is not a file");
51114
51495
  return resolution;
51115
51496
  }
51116
51497
  if (parsed.line !== undefined) {
51117
- const lineCount = readFileSync11(absolutePath, "utf-8").split(/\r?\n/).length;
51498
+ const lineCount = readFileSync12(absolutePath, "utf-8").split(/\r?\n/).length;
51118
51499
  if (parsed.line < 1 || parsed.line > lineCount) {
51119
51500
  resolution.warnings.push(`line ${parsed.line} is outside the file range 1-${lineCount}`);
51120
51501
  return resolution;
@@ -51146,7 +51527,7 @@ function walkSourceFiles(root, current = root, files = []) {
51146
51527
  if (!entry2.isFile())
51147
51528
  continue;
51148
51529
  const extension = `.${basename8(entry2.name).split(".").pop() || ""}`;
51149
- if (SOURCE_EXTENSIONS.has(extension) && statSync9(absolutePath).size <= 512 * 1024) {
51530
+ if (SOURCE_EXTENSIONS.has(extension) && statSync10(absolutePath).size <= 512 * 1024) {
51150
51531
  files.push(absolutePath);
51151
51532
  }
51152
51533
  }
@@ -51167,7 +51548,7 @@ function resolveSymbol(parsed, workspace, maxMatches) {
51167
51548
  const pattern = symbolPattern(name);
51168
51549
  const matches = [];
51169
51550
  for (const file of walkSourceFiles(workspace)) {
51170
- const lines = readFileSync11(file, "utf-8").split(/\r?\n/);
51551
+ const lines = readFileSync12(file, "utf-8").split(/\r?\n/);
51171
51552
  for (let index = 0;index < lines.length; index += 1) {
51172
51553
  const line = lines[index];
51173
51554
  const found = pattern.exec(line);
@@ -53909,7 +54290,7 @@ __export(exports_release_compatibility, {
53909
54290
  createReleaseCompatibilityReport: () => createReleaseCompatibilityReport,
53910
54291
  LOCAL_RELEASE_COMPATIBILITY_SCHEMA_VERSION: () => LOCAL_RELEASE_COMPATIBILITY_SCHEMA_VERSION
53911
54292
  });
53912
- import { readFileSync as readFileSync12 } from "fs";
54293
+ import { readFileSync as readFileSync13 } from "fs";
53913
54294
  import { join as join20, resolve as resolve19 } from "path";
53914
54295
  import { Database as Database3 } from "bun:sqlite";
53915
54296
  function pass(id, message, details) {
@@ -53922,7 +54303,7 @@ function warn(id, message, details) {
53922
54303
  return { id, status: "warning", message, details };
53923
54304
  }
53924
54305
  function readPackageJson2(root) {
53925
- return JSON.parse(readFileSync12(join20(root, "package.json"), "utf8"));
54306
+ return JSON.parse(readFileSync13(join20(root, "package.json"), "utf8"));
53926
54307
  }
53927
54308
  function sortedKeys(value) {
53928
54309
  return Object.keys(value ?? {}).sort((left, right) => left.localeCompare(right));
@@ -59892,7 +60273,7 @@ __export(exports_verification_providers, {
59892
60273
  getVerificationRecord: () => getVerificationRecord,
59893
60274
  discoverVerificationProviderCapabilities: () => discoverVerificationProviderCapabilities
59894
60275
  });
59895
- import { existsSync as existsSync20, readFileSync as readFileSync13 } from "fs";
60276
+ import { existsSync as existsSync20, readFileSync as readFileSync14 } from "fs";
59896
60277
  function normalizeName6(name) {
59897
60278
  const normalized = name.trim().toLowerCase();
59898
60279
  if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(normalized)) {
@@ -60044,7 +60425,7 @@ Timed out after ${provider.timeout_ms}ms`);
60044
60425
  };
60045
60426
  }
60046
60427
  function runCiLogProvider(input) {
60047
- const text = input.log_text ?? (input.log_path && existsSync20(input.log_path) ? readFileSync13(input.log_path, "utf-8") : "");
60428
+ const text = input.log_text ?? (input.log_path && existsSync20(input.log_path) ? readFileSync14(input.log_path, "utf-8") : "");
60048
60429
  return {
60049
60430
  status: classifyLog(text),
60050
60431
  attempts: 1,
@@ -62347,7 +62728,7 @@ __export(exports_local_backups, {
62347
62728
  LOCAL_BACKUP_CHECKSUM_ALGORITHM: () => LOCAL_BACKUP_CHECKSUM_ALGORITHM
62348
62729
  });
62349
62730
  import { createHash as createHash14 } from "crypto";
62350
- import { readFileSync as readFileSync14, writeFileSync as writeFileSync8 } from "fs";
62731
+ import { readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "fs";
62351
62732
  import { dirname as dirname9, resolve as resolve20 } from "path";
62352
62733
  import { mkdirSync as mkdirSync10 } from "fs";
62353
62734
  function stableJson2(value) {
@@ -62457,7 +62838,7 @@ function writeLocalBackupFile(backup, outputPath) {
62457
62838
  return path;
62458
62839
  }
62459
62840
  function readLocalBackupFile(path) {
62460
- return JSON.parse(readFileSync14(resolve20(path), "utf-8"));
62841
+ return JSON.parse(readFileSync15(resolve20(path), "utf-8"));
62461
62842
  }
62462
62843
  function verifyLocalBackup(value, options = {}, db) {
62463
62844
  const verifiedAt = options.verified_at ?? now();
@@ -63637,7 +64018,7 @@ __export(exports_agent_replay_simulator, {
63637
64018
  renderAgentReplaySimulationMarkdown: () => renderAgentReplaySimulationMarkdown
63638
64019
  });
63639
64020
  import { createHash as createHash16 } from "crypto";
63640
- import { readFileSync as readFileSync15 } from "fs";
64021
+ import { readFileSync as readFileSync16 } from "fs";
63641
64022
  function isObject2(value) {
63642
64023
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
63643
64024
  }
@@ -63870,7 +64251,7 @@ function simulateAgentReplay(input, options = {}) {
63870
64251
  };
63871
64252
  }
63872
64253
  function simulateAgentReplayFile(path, options = {}) {
63873
- const parsed = JSON.parse(readFileSync15(path, "utf8"));
64254
+ const parsed = JSON.parse(readFileSync16(path, "utf8"));
63874
64255
  return simulateAgentReplay(parsed, options);
63875
64256
  }
63876
64257
  function renderAgentReplaySimulationMarkdown(simulation) {
@@ -69209,7 +69590,7 @@ __export(exports_environment_snapshots, {
69209
69590
  captureEnvironmentSnapshot: () => captureEnvironmentSnapshot
69210
69591
  });
69211
69592
  import { createHash as createHash18 } from "crypto";
69212
- import { existsSync as existsSync21, readFileSync as readFileSync16, statSync as statSync10 } from "fs";
69593
+ import { existsSync as existsSync21, readFileSync as readFileSync17, statSync as statSync11 } from "fs";
69213
69594
  import { hostname as hostname2, platform, arch } from "os";
69214
69595
  import { dirname as dirname10, join as join22, resolve as resolve21 } from "path";
69215
69596
  import { tmpdir as tmpdir4 } from "os";
@@ -69220,10 +69601,10 @@ function fileRecord(root, relativePath) {
69220
69601
  const path = join22(root, relativePath);
69221
69602
  if (!existsSync21(path))
69222
69603
  return null;
69223
- const stat = statSync10(path);
69604
+ const stat = statSync11(path);
69224
69605
  if (!stat.isFile())
69225
69606
  return null;
69226
- const content = readFileSync16(path);
69607
+ const content = readFileSync17(path);
69227
69608
  return { path: relativePath, sha256: sha2567(content), size_bytes: content.length };
69228
69609
  }
69229
69610
  function manifestRecord(root, relativePath) {
@@ -72269,7 +72650,7 @@ __export(exports_config_serve_commands, {
72269
72650
  registerConfigServeCommands: () => registerConfigServeCommands
72270
72651
  });
72271
72652
  import chalk7 from "chalk";
72272
- import { existsSync as existsSync23, mkdirSync as mkdirSync12, readFileSync as readFileSync17, writeFileSync as writeFileSync10 } from "fs";
72653
+ import { existsSync as existsSync23, mkdirSync as mkdirSync12, readFileSync as readFileSync18, writeFileSync as writeFileSync10 } from "fs";
72273
72654
  import { dirname as dirname12, join as join24 } from "path";
72274
72655
  function registerConfigServeCommands(program2) {
72275
72656
  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) => {
@@ -72300,7 +72681,7 @@ function registerConfigServeCommands(program2) {
72300
72681
  }
72301
72682
  let config2 = {};
72302
72683
  try {
72303
- config2 = JSON.parse(readFileSync17(configPath, "utf-8"));
72684
+ config2 = JSON.parse(readFileSync18(configPath, "utf-8"));
72304
72685
  } catch {}
72305
72686
  const keys = key2.split(".");
72306
72687
  let obj = config2;
@@ -72440,7 +72821,7 @@ function registerConfigServeCommands(program2) {
72440
72821
  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) => {
72441
72822
  const globalOpts = program2.opts();
72442
72823
  const { listSecretFindings: listSecretFindings2 } = await Promise.resolve().then(() => (init_redaction(), exports_redaction));
72443
- const value = opts.file ? readFileSync17(opts.file, "utf-8") : text2 || "";
72824
+ const value = opts.file ? readFileSync18(opts.file, "utf-8") : text2 || "";
72444
72825
  const findings = listSecretFindings2(value);
72445
72826
  if (globalOpts.json) {
72446
72827
  output({ ok: findings.length === 0, findings }, true);
@@ -74006,7 +74387,7 @@ __export(exports_task_route_sources, {
74006
74387
  });
74007
74388
  import { Database as Database4 } from "bun:sqlite";
74008
74389
  import { createHash as createHash19 } from "crypto";
74009
- import { existsSync as existsSync25, readdirSync as readdirSync5, statSync as statSync11 } from "fs";
74390
+ import { existsSync as existsSync25, readdirSync as readdirSync5, statSync as statSync12 } from "fs";
74010
74391
  import { basename as basename10, dirname as dirname14, join as join26, resolve as resolve22 } from "path";
74011
74392
  function normalizePath6(input) {
74012
74393
  return resolve22(input);
@@ -74082,7 +74463,7 @@ function discoverStoresUnderRoot(sourceRoot) {
74082
74463
  }
74083
74464
  let rootStat;
74084
74465
  try {
74085
- rootStat = statSync11(rootPath);
74466
+ rootStat = statSync12(rootPath);
74086
74467
  } catch (error2) {
74087
74468
  const ref = createStoreRef(join26(rootPath, TODO_STORE_RELATIVE_PATH));
74088
74469
  errors2.push({
@@ -74815,7 +75196,7 @@ __export(exports_query_commands, {
74815
75196
  registerQueryCommands: () => registerQueryCommands
74816
75197
  });
74817
75198
  import chalk9 from "chalk";
74818
- import { readFileSync as readFileSync18, writeFileSync as writeFileSync12 } from "fs";
75199
+ import { readFileSync as readFileSync19, writeFileSync as writeFileSync12 } from "fs";
74819
75200
  function parseJsonObjectOption2(value, label) {
74820
75201
  if (!value)
74821
75202
  return;
@@ -75835,13 +76216,13 @@ Findings`));
75835
76216
  try {
75836
76217
  const db = getDatabase();
75837
76218
  const row = db.query("SELECT COUNT(*) as count FROM tasks").get();
75838
- const { statSync: statSync12 } = await import("fs");
76219
+ const { statSync: statSync13 } = await import("fs");
75839
76220
  const { join: join27 } = await import("path");
75840
76221
  const { getHomeDir: getHomeDir2 } = await Promise.resolve().then(() => (init_sync_utils(), exports_sync_utils));
75841
76222
  const dbPath = process.env["HASNA_TODOS_DB_PATH"] || process.env["TODOS_DB_PATH"] || join27(getHomeDir2(), ".hasna", "todos", "todos.db");
75842
76223
  let size = "unknown";
75843
76224
  try {
75844
- size = `${(statSync12(dbPath).size / 1024 / 1024).toFixed(1)} MB`;
76225
+ size = `${(statSync13(dbPath).size / 1024 / 1024).toFixed(1)} MB`;
75845
76226
  } catch {}
75846
76227
  checks.push({ name: "Database", ok: true, message: `${row.count} tasks \xB7 ${size} \xB7 ${chalk9.dim(dbPath)}` });
75847
76228
  } catch (e) {
@@ -76558,7 +76939,7 @@ Findings`));
76558
76939
  const sessionId = opts.session || globalOpts.session || undefined;
76559
76940
  try {
76560
76941
  if (opts.import) {
76561
- const bundle = JSON.parse(readFileSync18(opts.import, "utf-8"));
76942
+ const bundle = JSON.parse(readFileSync19(opts.import, "utf-8"));
76562
76943
  const result = importHandoffBundle(bundle, { apply: opts.apply }, db);
76563
76944
  if (opts.json || globalOpts.json) {
76564
76945
  console.log(JSON.stringify(result));
@@ -76932,7 +77313,7 @@ Findings`));
76932
77313
  });
76933
77314
  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) => {
76934
77315
  try {
76935
- const result = importCalendarIcs(readFileSync18(path, "utf-8"));
77316
+ const result = importCalendarIcs(readFileSync19(path, "utf-8"));
76936
77317
  if (opts.json || program2.opts().json) {
76937
77318
  output(result, true);
76938
77319
  return;
@@ -77083,7 +77464,7 @@ Findings`));
77083
77464
  });
77084
77465
  board.command("import <path>").description("Import local board definitions from a JSON bundle").option("-j, --json", "Output JSON").action((path, opts) => {
77085
77466
  try {
77086
- const bundle = JSON.parse(readFileSync18(path, "utf-8"));
77467
+ const bundle = JSON.parse(readFileSync19(path, "utf-8"));
77087
77468
  const result = importTaskBoardBundle(bundle);
77088
77469
  if (opts.json || program2.opts().json) {
77089
77470
  output(result, true);
@@ -77459,7 +77840,7 @@ Findings`));
77459
77840
  const { importExternalIssues: importExternalIssues2 } = await Promise.resolve().then(() => (init_external_issue_importers(), exports_external_issue_importers));
77460
77841
  let body2 = text2 || "";
77461
77842
  if (opts.file)
77462
- body2 = readFileSync18(opts.file, "utf-8");
77843
+ body2 = readFileSync19(opts.file, "utf-8");
77463
77844
  if (!body2 && !opts.url && !process.stdin.isTTY)
77464
77845
  body2 = await Bun.stdin.text();
77465
77846
  if (!body2.trim() && !opts.url) {
@@ -77516,7 +77897,7 @@ Findings`));
77516
77897
  } = await Promise.resolve().then(() => (init_tester_issue_reports(), exports_tester_issue_reports));
77517
77898
  let body2 = jsonText || "";
77518
77899
  if (opts.file)
77519
- body2 = readFileSync18(opts.file, "utf-8");
77900
+ body2 = readFileSync19(opts.file, "utf-8");
77520
77901
  if (!body2 && !process.stdin.isTTY)
77521
77902
  body2 = await Bun.stdin.text();
77522
77903
  if (!body2.trim()) {
@@ -77559,11 +77940,11 @@ Findings`));
77559
77940
  const inbox = program2.command("inbox").description("Capture local inbox items from pasted errors, CI logs, git context, files, or GitHub issue URLs");
77560
77941
  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) => {
77561
77942
  const globalOpts = program2.opts();
77562
- const { readFileSync: readFileSync19 } = await import("fs");
77943
+ const { readFileSync: readFileSync20 } = await import("fs");
77563
77944
  const { createInboxItem: createInboxItem2 } = await Promise.resolve().then(() => (init_inbox(), exports_inbox));
77564
77945
  let body2 = text2 || "";
77565
77946
  if (opts.file)
77566
- body2 = readFileSync19(opts.file, "utf-8");
77947
+ body2 = readFileSync20(opts.file, "utf-8");
77567
77948
  if (!body2 && !process.stdin.isTTY)
77568
77949
  body2 = await Bun.stdin.text();
77569
77950
  if (!body2.trim()) {
@@ -77623,11 +78004,11 @@ ${diff}` : null].filter(Boolean).join(`
77623
78004
  });
77624
78005
  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) => {
77625
78006
  const globalOpts = program2.opts();
77626
- const { readFileSync: readFileSync19 } = await import("fs");
78007
+ const { readFileSync: readFileSync20 } = await import("fs");
77627
78008
  const { previewNaturalLanguageIntake: previewNaturalLanguageIntake2 } = await Promise.resolve().then(() => (init_natural_language_intake(), exports_natural_language_intake));
77628
78009
  let body2 = text2 || "";
77629
78010
  if (opts.file)
77630
- body2 = readFileSync19(opts.file, "utf-8");
78011
+ body2 = readFileSync20(opts.file, "utf-8");
77631
78012
  if (!body2 && !process.stdin.isTTY)
77632
78013
  body2 = await Bun.stdin.text();
77633
78014
  if (!body2.trim()) {
@@ -77954,7 +78335,7 @@ __export(exports_mcp_hooks_commands, {
77954
78335
  });
77955
78336
  import chalk10 from "chalk";
77956
78337
  import { execSync as execSync3 } from "child_process";
77957
- import { existsSync as existsSync26, readFileSync as readFileSync19, writeFileSync as writeFileSync13, mkdirSync as mkdirSync14, chmodSync as chmodSync3 } from "fs";
78338
+ import { existsSync as existsSync26, readFileSync as readFileSync20, writeFileSync as writeFileSync13, mkdirSync as mkdirSync14, chmodSync as chmodSync3 } from "fs";
77958
78339
  import { dirname as dirname15, join as join27 } from "path";
77959
78340
  function getMcpBinaryPath() {
77960
78341
  try {
@@ -77971,7 +78352,7 @@ function readJsonFile2(path) {
77971
78352
  if (!existsSync26(path))
77972
78353
  return {};
77973
78354
  try {
77974
- return JSON.parse(readFileSync19(path, "utf-8"));
78355
+ return JSON.parse(readFileSync20(path, "utf-8"));
77975
78356
  } catch {
77976
78357
  return {};
77977
78358
  }
@@ -77986,7 +78367,7 @@ function writeJsonFile2(path, data) {
77986
78367
  function readTomlFile(path) {
77987
78368
  if (!existsSync26(path))
77988
78369
  return "";
77989
- return readFileSync19(path, "utf-8");
78370
+ return readFileSync20(path, "utf-8");
77990
78371
  }
77991
78372
  function writeTomlFile(path, content) {
77992
78373
  const dir = dirname15(path);
@@ -79177,7 +79558,7 @@ Artifacts:`));
79177
79558
  const hookPath = `${gitDir}/hooks/post-commit`;
79178
79559
  const marker = "# todos-auto-link";
79179
79560
  if (existsSync26(hookPath)) {
79180
- const existing = readFileSync19(hookPath, "utf-8");
79561
+ const existing = readFileSync20(hookPath, "utf-8");
79181
79562
  if (existing.includes(marker)) {
79182
79563
  console.log(chalk10.yellow("Hook already installed."));
79183
79564
  return;
@@ -79207,7 +79588,7 @@ $(dirname "$0")/../../scripts/post-commit-hook.sh
79207
79588
  console.log(chalk10.dim("No post-commit hook found."));
79208
79589
  return;
79209
79590
  }
79210
- const content = readFileSync19(hookPath, "utf-8");
79591
+ const content = readFileSync20(hookPath, "utf-8");
79211
79592
  if (!content.includes(marker)) {
79212
79593
  console.log(chalk10.dim("Hook not managed by todos."));
79213
79594
  return;
@@ -79458,7 +79839,7 @@ var STDIN_SENTINEL = "-";
79458
79839
  var init_delegation_brief = () => {};
79459
79840
 
79460
79841
  // src/lib/delegation-policy.ts
79461
- import { readFileSync as readFileSync20 } from "fs";
79842
+ import { readFileSync as readFileSync21 } from "fs";
79462
79843
  import { homedir as homedir4 } from "os";
79463
79844
  import { join as join28 } from "path";
79464
79845
  function defaultDelegationEmbargoPath() {
@@ -79466,7 +79847,7 @@ function defaultDelegationEmbargoPath() {
79466
79847
  }
79467
79848
  function loadDelegationEmbargo(path = defaultDelegationEmbargoPath()) {
79468
79849
  try {
79469
- const parsed = JSON.parse(readFileSync20(path, "utf8"));
79850
+ const parsed = JSON.parse(readFileSync21(path, "utf8"));
79470
79851
  const entries = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.embargoed) ? parsed.embargoed : [];
79471
79852
  const names = new Set;
79472
79853
  for (const entry2 of entries) {
@@ -79549,15 +79930,15 @@ __export(exports_delegate, {
79549
79930
  registerDelegateCommands: () => registerDelegateCommands
79550
79931
  });
79551
79932
  import chalk12 from "chalk";
79552
- import { readFileSync as readFileSync21 } from "fs";
79933
+ import { readFileSync as readFileSync22 } from "fs";
79553
79934
  function registerDelegateCommands(program2) {
79554
79935
  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) => {
79555
79936
  const globalOpts = program2.opts();
79556
79937
  const useJson = Boolean(opts.json || globalOpts.json);
79557
79938
  try {
79558
79939
  const brief = resolveDelegationBrief({ briefPath: opts.brief, briefText: opts.briefText }, {
79559
- readFile: (path) => readFileSync21(path, "utf8"),
79560
- readStdin: () => readFileSync21(0, "utf8")
79940
+ readFile: (path) => readFileSync22(path, "utf8"),
79941
+ readStdin: () => readFileSync22(0, "utf8")
79561
79942
  });
79562
79943
  if (!brief.ok)
79563
79944
  handleError(new Error(brief.message));
@@ -79805,7 +80186,7 @@ __export(exports_machines, {
79805
80186
  });
79806
80187
  import chalk13 from "chalk";
79807
80188
  import { execSync as execSync4 } from "child_process";
79808
- import { readFileSync as readFileSync22, unlinkSync as unlinkSync3, writeFileSync as writeFileSync14 } from "fs";
80189
+ import { readFileSync as readFileSync23, unlinkSync as unlinkSync3, writeFileSync as writeFileSync14 } from "fs";
79809
80190
  import { tmpdir as tmpdir5 } from "os";
79810
80191
  import { join as join29 } from "path";
79811
80192
  function getOrCreateLocalMachineName() {
@@ -79849,7 +80230,7 @@ function readRemoteBridgeBundle(sshAddress) {
79849
80230
  try {
79850
80231
  runSsh(sshAddress, `todos export --format bridge --allow-plaintext-sensitive --output ${shellQuote(remotePath)}`, 120000);
79851
80232
  scpFromRemote(sshAddress, remotePath, localPath);
79852
- return JSON.parse(readFileSync22(localPath, "utf-8"));
80233
+ return JSON.parse(readFileSync23(localPath, "utf-8"));
79853
80234
  } finally {
79854
80235
  try {
79855
80236
  runSsh(sshAddress, `rm -f ${shellQuote(remotePath)}`, 1e4);
@@ -84807,7 +85188,7 @@ var exports_roadmap_commands = {};
84807
85188
  __export(exports_roadmap_commands, {
84808
85189
  registerRoadmapCommands: () => registerRoadmapCommands
84809
85190
  });
84810
- import { readFileSync as readFileSync23, writeFileSync as writeFileSync16 } from "fs";
85191
+ import { readFileSync as readFileSync24, writeFileSync as writeFileSync16 } from "fs";
84811
85192
  import chalk24 from "chalk";
84812
85193
  function splitList3(value) {
84813
85194
  return value?.split(";").flatMap((part) => part.split(",")).map((item) => item.trim()).filter(Boolean);
@@ -85036,7 +85417,7 @@ function registerRoadmapCommands(program2) {
85036
85417
  const globalOpts = globalOptions(program2);
85037
85418
  try {
85038
85419
  const { importRoadmapBundle: importRoadmapBundle2 } = await Promise.resolve().then(() => (init_roadmaps(), exports_roadmaps));
85039
- const bundle = JSON.parse(readFileSync23(path, "utf8"));
85420
+ const bundle = JSON.parse(readFileSync24(path, "utf8"));
85040
85421
  const result = importRoadmapBundle2(bundle, { apply: Boolean(opts.apply) });
85041
85422
  if (globalOpts.json) {
85042
85423
  output(result, true);
@@ -85796,6 +86177,15 @@ function createShadowTodosStorageAdapter(options) {
85796
86177
  mirror.enqueueUpsert("plans", plan, context);
85797
86178
  return plan;
85798
86179
  },
86180
+ async completeAtRevision(id, expectedUpdatedAt, context) {
86181
+ if (typeof local.plans.completeAtRevision !== "function") {
86182
+ throw new Error("Atomic plan completion is not supported by the local shadow adapter");
86183
+ }
86184
+ const completed = await local.plans.completeAtRevision(id, expectedUpdatedAt, context);
86185
+ if (completed.applied)
86186
+ mirror.enqueueUpsert("plans", completed.plan, context);
86187
+ return completed;
86188
+ },
85799
86189
  async delete(id, context) {
85800
86190
  const deleted = await local.plans.delete(id, context);
85801
86191
  if (deleted)