@hasna/todos 0.15.18 → 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.18",
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 {
@@ -7043,7 +7145,7 @@ async function cloudTimeline(client, options = {}) {
7043
7145
  const limit = options.limit ?? 50;
7044
7146
  return { entries: entries.slice(offset, offset + limit), total, limit, offset };
7045
7147
  }
7046
- 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;
7047
7149
  var init_cloud_router = __esm(() => {
7048
7150
  init_types();
7049
7151
  init_redaction();
@@ -7068,10 +7170,23 @@ var init_cloud_router = __esm(() => {
7068
7170
  "confidence"
7069
7171
  ];
7070
7172
  completionCapabilityCache = new Map;
7173
+ retryCapabilityCache = new Map;
7071
7174
  gitRefCapabilityCache = new Map;
7072
7175
  SERVER_MODE_CANDIDATES = ["postgres", "cloud", "self_hosted"];
7073
7176
  PRIORITY_RANK = { critical: 0, high: 1, medium: 2, low: 3 };
7074
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
+ ];
7075
7190
  });
7076
7191
 
7077
7192
  // src/cli/stage-a.ts
@@ -18333,6 +18448,48 @@ function updatePlan(id, input, db) {
18333
18448
  return updatePlanStored(id, input, d);
18334
18449
  })();
18335
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
+ }
18336
18493
  function deletePlan(id, db) {
18337
18494
  const d = db || getDatabase();
18338
18495
  const plan = getPlan(id, d);
@@ -24233,7 +24390,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
24233
24390
  }
24234
24391
  if (result.errors.length > 0)
24235
24392
  return result;
24236
- const applyRows = (objectType, table, columns, rows, updateClockColumn, afterUpsert) => {
24393
+ const applyRows = (objectType, table, columns, rows, updateClockColumn, acceptEqualClock = true, afterUpsert) => {
24237
24394
  for (const row of rows) {
24238
24395
  try {
24239
24396
  const record = asRecord(row);
@@ -24242,7 +24399,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
24242
24399
  result.skipped += 1;
24243
24400
  continue;
24244
24401
  }
24245
- const state = upsertById(d, table, columns, record, updateClockColumn);
24402
+ const state = upsertById(d, table, columns, record, updateClockColumn, acceptEqualClock);
24246
24403
  if (state === "inserted")
24247
24404
  result.inserted += 1;
24248
24405
  else if (state === "updated")
@@ -24259,10 +24416,10 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
24259
24416
  applyRows("project_machine_paths", "project_machine_paths", PROJECT_MACHINE_PATH_COLUMNS, snapshot.projectMachinePaths ?? [], "updated_at");
24260
24417
  applyRows("agents", "agents", AGENT_COLUMNS, snapshot.agents, "last_seen_at");
24261
24418
  applyRows("task_lists", "task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
24262
- applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at");
24419
+ applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at", false);
24263
24420
  applyRows("templates", "task_templates", TEMPLATE_COLUMNS, snapshot.templates);
24264
24421
  applyRows("template_tasks", "template_tasks", TEMPLATE_TASK_COLUMNS, snapshot.templateTasks ?? []);
24265
- 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) => {
24266
24423
  if (changed && Array.isArray(row["tags"]) && typeof row["id"] === "string") {
24267
24424
  replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
24268
24425
  }
@@ -24271,7 +24428,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
24271
24428
  applyTombstones(d, snapshot.tombstones ?? [], result);
24272
24429
  return result;
24273
24430
  }
24274
- function upsertById(db, table, columns, row, updateClockColumn) {
24431
+ function upsertById(db, table, columns, row, updateClockColumn, acceptEqualClock = true) {
24275
24432
  const id = row["id"];
24276
24433
  if (typeof id !== "string" || !id)
24277
24434
  throw new Error(`${table} row is missing id`);
@@ -24283,7 +24440,7 @@ function upsertById(db, table, columns, row, updateClockColumn) {
24283
24440
  const values = presentColumns.map((column) => valueForColumn(column, row[column]));
24284
24441
  const updateColumns = presentColumns.filter((column) => column !== "id");
24285
24442
  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}` : "";
24443
+ const clockGuard = updateClockColumn && presentColumns.includes(updateClockColumn) ? ` WHERE ${table}.${updateClockColumn} IS NULL OR ${table}.${updateClockColumn} ${acceptEqualClock ? "<=" : "<"} excluded.${updateClockColumn}` : "";
24287
24444
  const sql = updateSet ? `INSERT INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders})
24288
24445
  ON CONFLICT(id) DO UPDATE SET ${updateSet}${clockGuard}` : `INSERT OR IGNORE INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders})`;
24289
24446
  const changes = db.run(sql, values).changes;
@@ -24702,6 +24859,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
24702
24859
  get: (id) => getPlan(id, database()),
24703
24860
  list: (projectId) => listPlans(projectId, database()),
24704
24861
  update: (id, input) => updatePlan(id, input, database()),
24862
+ completeAtRevision: (id, expectedUpdatedAt) => completePlanAtRevision(id, expectedUpdatedAt, database()),
24705
24863
  delete: (id) => deletePlan(id, database())
24706
24864
  },
24707
24865
  planProjectLinks: {
@@ -29234,6 +29392,7 @@ __export(exports_project_commands, {
29234
29392
  registerProjectCommands: () => registerProjectCommands
29235
29393
  });
29236
29394
  import chalk5 from "chalk";
29395
+ import { readFileSync as readFileSync9, statSync as statSync6 } from "fs";
29237
29396
  import { basename as basename5, resolve as resolve13, sep as sep3 } from "path";
29238
29397
  function collectOption(value, previous = []) {
29239
29398
  return [...previous, value];
@@ -29427,18 +29586,45 @@ function registerProjectCommands(program2) {
29427
29586
  handleError(e);
29428
29587
  }
29429
29588
  });
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) => {
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
+ }
29431
29618
  const globalOpts = program2.opts();
29432
29619
  const cloud = getTodosCloudClient();
29433
29620
  const resolvedId = await resolveTaskIdForCommand(id, cloud);
29434
- let content = text;
29435
29621
  let progressPct;
29436
29622
  if (opts.pct !== undefined) {
29437
29623
  const pct = parseInt(opts.pct, 10);
29438
29624
  if (!Number.isFinite(pct) || pct < 0 || pct > 100) {
29439
29625
  handleError(new Error("--pct must be a number between 0 and 100"));
29440
29626
  }
29441
- content = `[progress ${pct}%] ${text}`;
29627
+ content = `[progress ${pct}%] ${content}`;
29442
29628
  progressPct = pct;
29443
29629
  }
29444
29630
  const router = resolveWritableIdentity(globalOpts.agent);
@@ -30198,10 +30384,10 @@ Indexed ${result.index.files.length} file(s), ${result.index.total_symbols} symb
30198
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) => {
30199
30385
  const globalOpts = program2.opts();
30200
30386
  try {
30201
- const { readFileSync: readFileSync9 } = await import("fs");
30387
+ const { readFileSync: readFileSync10 } = await import("fs");
30202
30388
  const { importLocalBridgeBundle: importLocalBridgeBundle2 } = await Promise.resolve().then(() => (init_local_bridge(), exports_local_bridge));
30203
30389
  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"));
30390
+ const parsed = JSON.parse(readFileSync10(resolve13(file), "utf-8"));
30205
30391
  const bundle = isEncryptedBridgeBundle2(parsed) ? opts.decrypt ? decryptBridgeBundle2(parsed) : (() => {
30206
30392
  throw new Error("Bridge bundle is encrypted. Re-run with --decrypt and the configured key environment variable set.");
30207
30393
  })() : parsed;
@@ -30235,9 +30421,9 @@ Indexed ${result.index.files.length} file(s), ${result.index.total_symbols} symb
30235
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) => {
30236
30422
  const globalOpts = program2.opts();
30237
30423
  try {
30238
- const { readFileSync: readFileSync9 } = await import("fs");
30424
+ const { readFileSync: readFileSync10 } = await import("fs");
30239
30425
  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" });
30426
+ const result = importTodosMarkdown2(readFileSync10(resolve13(file), "utf-8"), { dryRun: !opts.apply, conflictStrategy: opts.resolveConflicts ? "safe_merge" : "skip" });
30241
30427
  const { emitLocalEventHooksQuiet: emitLocalEventHooksQuiet2 } = await Promise.resolve().then(() => (init_event_hooks(), exports_event_hooks));
30242
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 } });
30243
30429
  if (globalOpts.json) {
@@ -30884,7 +31070,7 @@ async function findFreePort(start) {
30884
31070
  var DEFAULT_PORT = 19427;
30885
31071
 
30886
31072
  // 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";
31073
+ import { existsSync as existsSync15, copyFileSync, mkdirSync as mkdirSync8, readFileSync as readFileSync10, renameSync, statSync as statSync7, writeFileSync as writeFileSync7, unlinkSync } from "fs";
30888
31074
  import { dirname as dirname7, join as join14, resolve as resolve14 } from "path";
30889
31075
  import { Database as Database2 } from "bun:sqlite";
30890
31076
  function resolveDbPath(dbPath) {
@@ -30913,7 +31099,7 @@ function backupDatabase(outputPath, sourcePath) {
30913
31099
  src.close();
30914
31100
  writeFileSync7(outputPath, image);
30915
31101
  const method = "file_copy";
30916
- const bytes = statSync6(outputPath).size;
31102
+ const bytes = statSync7(outputPath).size;
30917
31103
  return {
30918
31104
  schema_version: DB_BACKUP_SCHEMA,
30919
31105
  source_path: source,
@@ -32250,7 +32436,7 @@ __export(exports_local_extensions, {
32250
32436
  discoverLocalExtensions: () => discoverLocalExtensions
32251
32437
  });
32252
32438
  import { createHash as createHash9, createVerify } from "crypto";
32253
- 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";
32254
32440
  import { basename as basename6, join as join16, resolve as resolve15 } from "path";
32255
32441
  function isObject(value) {
32256
32442
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
@@ -32332,7 +32518,7 @@ function normalizeManifest(input) {
32332
32518
  };
32333
32519
  }
32334
32520
  function parseJson(path) {
32335
- return JSON.parse(readFileSync10(path, "utf8"));
32521
+ return JSON.parse(readFileSync11(path, "utf8"));
32336
32522
  }
32337
32523
  function sha2564(bytes) {
32338
32524
  return `sha256:${createHash9("sha256").update(bytes).digest("hex")}`;
@@ -32512,11 +32698,11 @@ function inspectExtensionSource(source2) {
32512
32698
  const resolved = resolve15(source2);
32513
32699
  if (!existsSync17(resolved))
32514
32700
  throw new Error(`extension source not found: ${source2}`);
32515
- const stat = statSync7(resolved);
32701
+ const stat = statSync8(resolved);
32516
32702
  const manifestPath = stat.isDirectory() ? [join16(resolved, "todos.extension.json"), join16(resolved, "extension.json")].find(existsSync17) : resolved;
32517
32703
  if (!manifestPath)
32518
32704
  throw new Error(`extension directory ${source2} is missing todos.extension.json`);
32519
- const raw = readFileSync10(manifestPath);
32705
+ const raw = readFileSync11(manifestPath);
32520
32706
  const parsed = parseJson(manifestPath);
32521
32707
  const bundle = isObject(parsed) && isObject(parsed["manifest"]);
32522
32708
  const manifest = normalizeManifest(bundle ? parsed["manifest"] : parsed);
@@ -32618,7 +32804,7 @@ function projectExtensionSources(projectPath) {
32618
32804
  if (entry.startsWith("."))
32619
32805
  continue;
32620
32806
  const full = join16(extensionDir, entry);
32621
- if (statSync7(full).isDirectory() || entry.endsWith(".json"))
32807
+ if (statSync8(full).isDirectory() || entry.endsWith(".json"))
32622
32808
  candidates.push(full);
32623
32809
  }
32624
32810
  }
@@ -34323,6 +34509,7 @@ function createPostgresTodosStorageAdapter(options) {
34323
34509
  get: (id) => store.get("plans", id),
34324
34510
  list: async (projectId) => (await store.list("plans")).filter((plan) => projectId === undefined || plan.project_id === projectId).sort((a, b) => a.name.localeCompare(b.name)),
34325
34511
  update: (id, input) => updatePlan2(id, input, store),
34512
+ completeAtRevision: (id, expectedUpdatedAt, context) => store.completePlanAtRevision(id, expectedUpdatedAt, context),
34326
34513
  delete: (id, context) => store.deletePlan(id, context)
34327
34514
  },
34328
34515
  planProjectLinks: {
@@ -34810,6 +34997,54 @@ class PostgresJsonRecordStore {
34810
34997
  throw new PlanNotFoundError(value.id);
34811
34998
  return payloadRecord2(row.payload);
34812
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
+ }
34813
35048
  async createTemplateWithTasks(template, tasks, context = {}) {
34814
35049
  await this.ensureSchema();
34815
35050
  const records = [
@@ -44645,7 +44880,7 @@ var exports_doctor = {};
44645
44880
  __export(exports_doctor, {
44646
44881
  runTodosDoctor: () => runTodosDoctor
44647
44882
  });
44648
- 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";
44649
44884
  import { basename as basename7, dirname as dirname8, join as join17 } from "path";
44650
44885
  function tableExists3(db, table) {
44651
44886
  return Boolean(db.query("SELECT name FROM sqlite_master WHERE type='table' AND name=?").get(table));
@@ -44792,7 +45027,7 @@ function databasePermissionsAreUnsafe(dbPath) {
44792
45027
  if (dbPath === ":memory:" || dbPath.startsWith("file::memory:"))
44793
45028
  return false;
44794
45029
  try {
44795
- return (statSync8(dbPath).mode & 63) !== 0;
45030
+ return (statSync9(dbPath).mode & 63) !== 0;
44796
45031
  } catch {
44797
45032
  return false;
44798
45033
  }
@@ -48010,8 +48245,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
48010
48245
  "/v1/import": {
48011
48246
  post: {
48012
48247
  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.",
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.",
48015
48250
  requestBody: {
48016
48251
  required: true,
48017
48252
  content: {
@@ -48030,7 +48265,22 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
48030
48265
  templates: { type: "array", items: { type: "object" } },
48031
48266
  templateTasks: { type: "array", items: { $ref: "#/components/schemas/TemplateTask" } },
48032
48267
  auditHistory: { type: "array", items: { type: "object" } },
48033
- 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
+ }
48034
48284
  }
48035
48285
  }
48036
48286
  }
@@ -48053,6 +48303,26 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
48053
48303
  skipped: { type: "number" },
48054
48304
  errors: { type: "array", items: { type: "string" } }
48055
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
+ }
48056
48326
  }
48057
48327
  }
48058
48328
  }
@@ -48902,6 +49172,65 @@ function normalizeImportSnapshot(raw) {
48902
49172
  function countSnapshotRecords(s) {
48903
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);
48904
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
+ }
48905
49234
  async function handleV1Request(req, url, dependencies = {}) {
48906
49235
  const path = url.pathname;
48907
49236
  if (path !== "/v1" && !path.startsWith("/v1/"))
@@ -49838,6 +50167,36 @@ async function handleV1Request(req, url, dependencies = {}) {
49838
50167
  return error(400, "invalid JSON body");
49839
50168
  const snapshot = normalizeImportSnapshot(raw);
49840
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
+ }
49841
50200
  if (received === 0) {
49842
50201
  return error(400, "empty snapshot: provide at least one record array (tasks/projects/plans/...)");
49843
50202
  }
@@ -49864,6 +50223,18 @@ async function handleV1Request(req, url, dependencies = {}) {
49864
50223
  if (e instanceof TaskNotFoundError) {
49865
50224
  return error(404, e.message, { code: TaskNotFoundError.code });
49866
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
+ }
49867
50238
  if (e instanceof LockError)
49868
50239
  return error(409, e.message, { code: LockError.code });
49869
50240
  if (e instanceof TaskNotStartableError) {
@@ -51015,7 +51386,7 @@ var exports_mention_resolver = {};
51015
51386
  __export(exports_mention_resolver, {
51016
51387
  resolveMentions: () => resolveMentions
51017
51388
  });
51018
- 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";
51019
51390
  import { basename as basename8, isAbsolute, join as join19, relative as relative5, resolve as resolve18, sep as sep5 } from "path";
51020
51391
  function blankResolution(parsed) {
51021
51392
  return {
@@ -51118,13 +51489,13 @@ function resolveFile(parsed, workspace) {
51118
51489
  resolution.warnings.push("file does not exist in the local workspace");
51119
51490
  return resolution;
51120
51491
  }
51121
- const stats = statSync9(absolutePath);
51492
+ const stats = statSync10(absolutePath);
51122
51493
  if (!stats.isFile()) {
51123
51494
  resolution.warnings.push("path exists but is not a file");
51124
51495
  return resolution;
51125
51496
  }
51126
51497
  if (parsed.line !== undefined) {
51127
- const lineCount = readFileSync11(absolutePath, "utf-8").split(/\r?\n/).length;
51498
+ const lineCount = readFileSync12(absolutePath, "utf-8").split(/\r?\n/).length;
51128
51499
  if (parsed.line < 1 || parsed.line > lineCount) {
51129
51500
  resolution.warnings.push(`line ${parsed.line} is outside the file range 1-${lineCount}`);
51130
51501
  return resolution;
@@ -51156,7 +51527,7 @@ function walkSourceFiles(root, current = root, files = []) {
51156
51527
  if (!entry2.isFile())
51157
51528
  continue;
51158
51529
  const extension = `.${basename8(entry2.name).split(".").pop() || ""}`;
51159
- if (SOURCE_EXTENSIONS.has(extension) && statSync9(absolutePath).size <= 512 * 1024) {
51530
+ if (SOURCE_EXTENSIONS.has(extension) && statSync10(absolutePath).size <= 512 * 1024) {
51160
51531
  files.push(absolutePath);
51161
51532
  }
51162
51533
  }
@@ -51177,7 +51548,7 @@ function resolveSymbol(parsed, workspace, maxMatches) {
51177
51548
  const pattern = symbolPattern(name);
51178
51549
  const matches = [];
51179
51550
  for (const file of walkSourceFiles(workspace)) {
51180
- const lines = readFileSync11(file, "utf-8").split(/\r?\n/);
51551
+ const lines = readFileSync12(file, "utf-8").split(/\r?\n/);
51181
51552
  for (let index = 0;index < lines.length; index += 1) {
51182
51553
  const line = lines[index];
51183
51554
  const found = pattern.exec(line);
@@ -53919,7 +54290,7 @@ __export(exports_release_compatibility, {
53919
54290
  createReleaseCompatibilityReport: () => createReleaseCompatibilityReport,
53920
54291
  LOCAL_RELEASE_COMPATIBILITY_SCHEMA_VERSION: () => LOCAL_RELEASE_COMPATIBILITY_SCHEMA_VERSION
53921
54292
  });
53922
- import { readFileSync as readFileSync12 } from "fs";
54293
+ import { readFileSync as readFileSync13 } from "fs";
53923
54294
  import { join as join20, resolve as resolve19 } from "path";
53924
54295
  import { Database as Database3 } from "bun:sqlite";
53925
54296
  function pass(id, message, details) {
@@ -53932,7 +54303,7 @@ function warn(id, message, details) {
53932
54303
  return { id, status: "warning", message, details };
53933
54304
  }
53934
54305
  function readPackageJson2(root) {
53935
- return JSON.parse(readFileSync12(join20(root, "package.json"), "utf8"));
54306
+ return JSON.parse(readFileSync13(join20(root, "package.json"), "utf8"));
53936
54307
  }
53937
54308
  function sortedKeys(value) {
53938
54309
  return Object.keys(value ?? {}).sort((left, right) => left.localeCompare(right));
@@ -59902,7 +60273,7 @@ __export(exports_verification_providers, {
59902
60273
  getVerificationRecord: () => getVerificationRecord,
59903
60274
  discoverVerificationProviderCapabilities: () => discoverVerificationProviderCapabilities
59904
60275
  });
59905
- import { existsSync as existsSync20, readFileSync as readFileSync13 } from "fs";
60276
+ import { existsSync as existsSync20, readFileSync as readFileSync14 } from "fs";
59906
60277
  function normalizeName6(name) {
59907
60278
  const normalized = name.trim().toLowerCase();
59908
60279
  if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(normalized)) {
@@ -60054,7 +60425,7 @@ Timed out after ${provider.timeout_ms}ms`);
60054
60425
  };
60055
60426
  }
60056
60427
  function runCiLogProvider(input) {
60057
- 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") : "");
60058
60429
  return {
60059
60430
  status: classifyLog(text),
60060
60431
  attempts: 1,
@@ -62357,7 +62728,7 @@ __export(exports_local_backups, {
62357
62728
  LOCAL_BACKUP_CHECKSUM_ALGORITHM: () => LOCAL_BACKUP_CHECKSUM_ALGORITHM
62358
62729
  });
62359
62730
  import { createHash as createHash14 } from "crypto";
62360
- import { readFileSync as readFileSync14, writeFileSync as writeFileSync8 } from "fs";
62731
+ import { readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "fs";
62361
62732
  import { dirname as dirname9, resolve as resolve20 } from "path";
62362
62733
  import { mkdirSync as mkdirSync10 } from "fs";
62363
62734
  function stableJson2(value) {
@@ -62467,7 +62838,7 @@ function writeLocalBackupFile(backup, outputPath) {
62467
62838
  return path;
62468
62839
  }
62469
62840
  function readLocalBackupFile(path) {
62470
- return JSON.parse(readFileSync14(resolve20(path), "utf-8"));
62841
+ return JSON.parse(readFileSync15(resolve20(path), "utf-8"));
62471
62842
  }
62472
62843
  function verifyLocalBackup(value, options = {}, db) {
62473
62844
  const verifiedAt = options.verified_at ?? now();
@@ -63647,7 +64018,7 @@ __export(exports_agent_replay_simulator, {
63647
64018
  renderAgentReplaySimulationMarkdown: () => renderAgentReplaySimulationMarkdown
63648
64019
  });
63649
64020
  import { createHash as createHash16 } from "crypto";
63650
- import { readFileSync as readFileSync15 } from "fs";
64021
+ import { readFileSync as readFileSync16 } from "fs";
63651
64022
  function isObject2(value) {
63652
64023
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
63653
64024
  }
@@ -63880,7 +64251,7 @@ function simulateAgentReplay(input, options = {}) {
63880
64251
  };
63881
64252
  }
63882
64253
  function simulateAgentReplayFile(path, options = {}) {
63883
- const parsed = JSON.parse(readFileSync15(path, "utf8"));
64254
+ const parsed = JSON.parse(readFileSync16(path, "utf8"));
63884
64255
  return simulateAgentReplay(parsed, options);
63885
64256
  }
63886
64257
  function renderAgentReplaySimulationMarkdown(simulation) {
@@ -69219,7 +69590,7 @@ __export(exports_environment_snapshots, {
69219
69590
  captureEnvironmentSnapshot: () => captureEnvironmentSnapshot
69220
69591
  });
69221
69592
  import { createHash as createHash18 } from "crypto";
69222
- 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";
69223
69594
  import { hostname as hostname2, platform, arch } from "os";
69224
69595
  import { dirname as dirname10, join as join22, resolve as resolve21 } from "path";
69225
69596
  import { tmpdir as tmpdir4 } from "os";
@@ -69230,10 +69601,10 @@ function fileRecord(root, relativePath) {
69230
69601
  const path = join22(root, relativePath);
69231
69602
  if (!existsSync21(path))
69232
69603
  return null;
69233
- const stat = statSync10(path);
69604
+ const stat = statSync11(path);
69234
69605
  if (!stat.isFile())
69235
69606
  return null;
69236
- const content = readFileSync16(path);
69607
+ const content = readFileSync17(path);
69237
69608
  return { path: relativePath, sha256: sha2567(content), size_bytes: content.length };
69238
69609
  }
69239
69610
  function manifestRecord(root, relativePath) {
@@ -72279,7 +72650,7 @@ __export(exports_config_serve_commands, {
72279
72650
  registerConfigServeCommands: () => registerConfigServeCommands
72280
72651
  });
72281
72652
  import chalk7 from "chalk";
72282
- 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";
72283
72654
  import { dirname as dirname12, join as join24 } from "path";
72284
72655
  function registerConfigServeCommands(program2) {
72285
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) => {
@@ -72310,7 +72681,7 @@ function registerConfigServeCommands(program2) {
72310
72681
  }
72311
72682
  let config2 = {};
72312
72683
  try {
72313
- config2 = JSON.parse(readFileSync17(configPath, "utf-8"));
72684
+ config2 = JSON.parse(readFileSync18(configPath, "utf-8"));
72314
72685
  } catch {}
72315
72686
  const keys = key2.split(".");
72316
72687
  let obj = config2;
@@ -72450,7 +72821,7 @@ function registerConfigServeCommands(program2) {
72450
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) => {
72451
72822
  const globalOpts = program2.opts();
72452
72823
  const { listSecretFindings: listSecretFindings2 } = await Promise.resolve().then(() => (init_redaction(), exports_redaction));
72453
- const value = opts.file ? readFileSync17(opts.file, "utf-8") : text2 || "";
72824
+ const value = opts.file ? readFileSync18(opts.file, "utf-8") : text2 || "";
72454
72825
  const findings = listSecretFindings2(value);
72455
72826
  if (globalOpts.json) {
72456
72827
  output({ ok: findings.length === 0, findings }, true);
@@ -74016,7 +74387,7 @@ __export(exports_task_route_sources, {
74016
74387
  });
74017
74388
  import { Database as Database4 } from "bun:sqlite";
74018
74389
  import { createHash as createHash19 } from "crypto";
74019
- 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";
74020
74391
  import { basename as basename10, dirname as dirname14, join as join26, resolve as resolve22 } from "path";
74021
74392
  function normalizePath6(input) {
74022
74393
  return resolve22(input);
@@ -74092,7 +74463,7 @@ function discoverStoresUnderRoot(sourceRoot) {
74092
74463
  }
74093
74464
  let rootStat;
74094
74465
  try {
74095
- rootStat = statSync11(rootPath);
74466
+ rootStat = statSync12(rootPath);
74096
74467
  } catch (error2) {
74097
74468
  const ref = createStoreRef(join26(rootPath, TODO_STORE_RELATIVE_PATH));
74098
74469
  errors2.push({
@@ -74825,7 +75196,7 @@ __export(exports_query_commands, {
74825
75196
  registerQueryCommands: () => registerQueryCommands
74826
75197
  });
74827
75198
  import chalk9 from "chalk";
74828
- import { readFileSync as readFileSync18, writeFileSync as writeFileSync12 } from "fs";
75199
+ import { readFileSync as readFileSync19, writeFileSync as writeFileSync12 } from "fs";
74829
75200
  function parseJsonObjectOption2(value, label) {
74830
75201
  if (!value)
74831
75202
  return;
@@ -75845,13 +76216,13 @@ Findings`));
75845
76216
  try {
75846
76217
  const db = getDatabase();
75847
76218
  const row = db.query("SELECT COUNT(*) as count FROM tasks").get();
75848
- const { statSync: statSync12 } = await import("fs");
76219
+ const { statSync: statSync13 } = await import("fs");
75849
76220
  const { join: join27 } = await import("path");
75850
76221
  const { getHomeDir: getHomeDir2 } = await Promise.resolve().then(() => (init_sync_utils(), exports_sync_utils));
75851
76222
  const dbPath = process.env["HASNA_TODOS_DB_PATH"] || process.env["TODOS_DB_PATH"] || join27(getHomeDir2(), ".hasna", "todos", "todos.db");
75852
76223
  let size = "unknown";
75853
76224
  try {
75854
- size = `${(statSync12(dbPath).size / 1024 / 1024).toFixed(1)} MB`;
76225
+ size = `${(statSync13(dbPath).size / 1024 / 1024).toFixed(1)} MB`;
75855
76226
  } catch {}
75856
76227
  checks.push({ name: "Database", ok: true, message: `${row.count} tasks \xB7 ${size} \xB7 ${chalk9.dim(dbPath)}` });
75857
76228
  } catch (e) {
@@ -76568,7 +76939,7 @@ Findings`));
76568
76939
  const sessionId = opts.session || globalOpts.session || undefined;
76569
76940
  try {
76570
76941
  if (opts.import) {
76571
- const bundle = JSON.parse(readFileSync18(opts.import, "utf-8"));
76942
+ const bundle = JSON.parse(readFileSync19(opts.import, "utf-8"));
76572
76943
  const result = importHandoffBundle(bundle, { apply: opts.apply }, db);
76573
76944
  if (opts.json || globalOpts.json) {
76574
76945
  console.log(JSON.stringify(result));
@@ -76942,7 +77313,7 @@ Findings`));
76942
77313
  });
76943
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) => {
76944
77315
  try {
76945
- const result = importCalendarIcs(readFileSync18(path, "utf-8"));
77316
+ const result = importCalendarIcs(readFileSync19(path, "utf-8"));
76946
77317
  if (opts.json || program2.opts().json) {
76947
77318
  output(result, true);
76948
77319
  return;
@@ -77093,7 +77464,7 @@ Findings`));
77093
77464
  });
77094
77465
  board.command("import <path>").description("Import local board definitions from a JSON bundle").option("-j, --json", "Output JSON").action((path, opts) => {
77095
77466
  try {
77096
- const bundle = JSON.parse(readFileSync18(path, "utf-8"));
77467
+ const bundle = JSON.parse(readFileSync19(path, "utf-8"));
77097
77468
  const result = importTaskBoardBundle(bundle);
77098
77469
  if (opts.json || program2.opts().json) {
77099
77470
  output(result, true);
@@ -77469,7 +77840,7 @@ Findings`));
77469
77840
  const { importExternalIssues: importExternalIssues2 } = await Promise.resolve().then(() => (init_external_issue_importers(), exports_external_issue_importers));
77470
77841
  let body2 = text2 || "";
77471
77842
  if (opts.file)
77472
- body2 = readFileSync18(opts.file, "utf-8");
77843
+ body2 = readFileSync19(opts.file, "utf-8");
77473
77844
  if (!body2 && !opts.url && !process.stdin.isTTY)
77474
77845
  body2 = await Bun.stdin.text();
77475
77846
  if (!body2.trim() && !opts.url) {
@@ -77526,7 +77897,7 @@ Findings`));
77526
77897
  } = await Promise.resolve().then(() => (init_tester_issue_reports(), exports_tester_issue_reports));
77527
77898
  let body2 = jsonText || "";
77528
77899
  if (opts.file)
77529
- body2 = readFileSync18(opts.file, "utf-8");
77900
+ body2 = readFileSync19(opts.file, "utf-8");
77530
77901
  if (!body2 && !process.stdin.isTTY)
77531
77902
  body2 = await Bun.stdin.text();
77532
77903
  if (!body2.trim()) {
@@ -77569,11 +77940,11 @@ Findings`));
77569
77940
  const inbox = program2.command("inbox").description("Capture local inbox items from pasted errors, CI logs, git context, files, or GitHub issue URLs");
77570
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) => {
77571
77942
  const globalOpts = program2.opts();
77572
- const { readFileSync: readFileSync19 } = await import("fs");
77943
+ const { readFileSync: readFileSync20 } = await import("fs");
77573
77944
  const { createInboxItem: createInboxItem2 } = await Promise.resolve().then(() => (init_inbox(), exports_inbox));
77574
77945
  let body2 = text2 || "";
77575
77946
  if (opts.file)
77576
- body2 = readFileSync19(opts.file, "utf-8");
77947
+ body2 = readFileSync20(opts.file, "utf-8");
77577
77948
  if (!body2 && !process.stdin.isTTY)
77578
77949
  body2 = await Bun.stdin.text();
77579
77950
  if (!body2.trim()) {
@@ -77633,11 +78004,11 @@ ${diff}` : null].filter(Boolean).join(`
77633
78004
  });
77634
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) => {
77635
78006
  const globalOpts = program2.opts();
77636
- const { readFileSync: readFileSync19 } = await import("fs");
78007
+ const { readFileSync: readFileSync20 } = await import("fs");
77637
78008
  const { previewNaturalLanguageIntake: previewNaturalLanguageIntake2 } = await Promise.resolve().then(() => (init_natural_language_intake(), exports_natural_language_intake));
77638
78009
  let body2 = text2 || "";
77639
78010
  if (opts.file)
77640
- body2 = readFileSync19(opts.file, "utf-8");
78011
+ body2 = readFileSync20(opts.file, "utf-8");
77641
78012
  if (!body2 && !process.stdin.isTTY)
77642
78013
  body2 = await Bun.stdin.text();
77643
78014
  if (!body2.trim()) {
@@ -77964,7 +78335,7 @@ __export(exports_mcp_hooks_commands, {
77964
78335
  });
77965
78336
  import chalk10 from "chalk";
77966
78337
  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";
78338
+ import { existsSync as existsSync26, readFileSync as readFileSync20, writeFileSync as writeFileSync13, mkdirSync as mkdirSync14, chmodSync as chmodSync3 } from "fs";
77968
78339
  import { dirname as dirname15, join as join27 } from "path";
77969
78340
  function getMcpBinaryPath() {
77970
78341
  try {
@@ -77981,7 +78352,7 @@ function readJsonFile2(path) {
77981
78352
  if (!existsSync26(path))
77982
78353
  return {};
77983
78354
  try {
77984
- return JSON.parse(readFileSync19(path, "utf-8"));
78355
+ return JSON.parse(readFileSync20(path, "utf-8"));
77985
78356
  } catch {
77986
78357
  return {};
77987
78358
  }
@@ -77996,7 +78367,7 @@ function writeJsonFile2(path, data) {
77996
78367
  function readTomlFile(path) {
77997
78368
  if (!existsSync26(path))
77998
78369
  return "";
77999
- return readFileSync19(path, "utf-8");
78370
+ return readFileSync20(path, "utf-8");
78000
78371
  }
78001
78372
  function writeTomlFile(path, content) {
78002
78373
  const dir = dirname15(path);
@@ -79187,7 +79558,7 @@ Artifacts:`));
79187
79558
  const hookPath = `${gitDir}/hooks/post-commit`;
79188
79559
  const marker = "# todos-auto-link";
79189
79560
  if (existsSync26(hookPath)) {
79190
- const existing = readFileSync19(hookPath, "utf-8");
79561
+ const existing = readFileSync20(hookPath, "utf-8");
79191
79562
  if (existing.includes(marker)) {
79192
79563
  console.log(chalk10.yellow("Hook already installed."));
79193
79564
  return;
@@ -79217,7 +79588,7 @@ $(dirname "$0")/../../scripts/post-commit-hook.sh
79217
79588
  console.log(chalk10.dim("No post-commit hook found."));
79218
79589
  return;
79219
79590
  }
79220
- const content = readFileSync19(hookPath, "utf-8");
79591
+ const content = readFileSync20(hookPath, "utf-8");
79221
79592
  if (!content.includes(marker)) {
79222
79593
  console.log(chalk10.dim("Hook not managed by todos."));
79223
79594
  return;
@@ -79468,7 +79839,7 @@ var STDIN_SENTINEL = "-";
79468
79839
  var init_delegation_brief = () => {};
79469
79840
 
79470
79841
  // src/lib/delegation-policy.ts
79471
- import { readFileSync as readFileSync20 } from "fs";
79842
+ import { readFileSync as readFileSync21 } from "fs";
79472
79843
  import { homedir as homedir4 } from "os";
79473
79844
  import { join as join28 } from "path";
79474
79845
  function defaultDelegationEmbargoPath() {
@@ -79476,7 +79847,7 @@ function defaultDelegationEmbargoPath() {
79476
79847
  }
79477
79848
  function loadDelegationEmbargo(path = defaultDelegationEmbargoPath()) {
79478
79849
  try {
79479
- const parsed = JSON.parse(readFileSync20(path, "utf8"));
79850
+ const parsed = JSON.parse(readFileSync21(path, "utf8"));
79480
79851
  const entries = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.embargoed) ? parsed.embargoed : [];
79481
79852
  const names = new Set;
79482
79853
  for (const entry2 of entries) {
@@ -79559,15 +79930,15 @@ __export(exports_delegate, {
79559
79930
  registerDelegateCommands: () => registerDelegateCommands
79560
79931
  });
79561
79932
  import chalk12 from "chalk";
79562
- import { readFileSync as readFileSync21 } from "fs";
79933
+ import { readFileSync as readFileSync22 } from "fs";
79563
79934
  function registerDelegateCommands(program2) {
79564
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) => {
79565
79936
  const globalOpts = program2.opts();
79566
79937
  const useJson = Boolean(opts.json || globalOpts.json);
79567
79938
  try {
79568
79939
  const brief = resolveDelegationBrief({ briefPath: opts.brief, briefText: opts.briefText }, {
79569
- readFile: (path) => readFileSync21(path, "utf8"),
79570
- readStdin: () => readFileSync21(0, "utf8")
79940
+ readFile: (path) => readFileSync22(path, "utf8"),
79941
+ readStdin: () => readFileSync22(0, "utf8")
79571
79942
  });
79572
79943
  if (!brief.ok)
79573
79944
  handleError(new Error(brief.message));
@@ -79815,7 +80186,7 @@ __export(exports_machines, {
79815
80186
  });
79816
80187
  import chalk13 from "chalk";
79817
80188
  import { execSync as execSync4 } from "child_process";
79818
- 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";
79819
80190
  import { tmpdir as tmpdir5 } from "os";
79820
80191
  import { join as join29 } from "path";
79821
80192
  function getOrCreateLocalMachineName() {
@@ -79859,7 +80230,7 @@ function readRemoteBridgeBundle(sshAddress) {
79859
80230
  try {
79860
80231
  runSsh(sshAddress, `todos export --format bridge --allow-plaintext-sensitive --output ${shellQuote(remotePath)}`, 120000);
79861
80232
  scpFromRemote(sshAddress, remotePath, localPath);
79862
- return JSON.parse(readFileSync22(localPath, "utf-8"));
80233
+ return JSON.parse(readFileSync23(localPath, "utf-8"));
79863
80234
  } finally {
79864
80235
  try {
79865
80236
  runSsh(sshAddress, `rm -f ${shellQuote(remotePath)}`, 1e4);
@@ -84817,7 +85188,7 @@ var exports_roadmap_commands = {};
84817
85188
  __export(exports_roadmap_commands, {
84818
85189
  registerRoadmapCommands: () => registerRoadmapCommands
84819
85190
  });
84820
- import { readFileSync as readFileSync23, writeFileSync as writeFileSync16 } from "fs";
85191
+ import { readFileSync as readFileSync24, writeFileSync as writeFileSync16 } from "fs";
84821
85192
  import chalk24 from "chalk";
84822
85193
  function splitList3(value) {
84823
85194
  return value?.split(";").flatMap((part) => part.split(",")).map((item) => item.trim()).filter(Boolean);
@@ -85046,7 +85417,7 @@ function registerRoadmapCommands(program2) {
85046
85417
  const globalOpts = globalOptions(program2);
85047
85418
  try {
85048
85419
  const { importRoadmapBundle: importRoadmapBundle2 } = await Promise.resolve().then(() => (init_roadmaps(), exports_roadmaps));
85049
- const bundle = JSON.parse(readFileSync23(path, "utf8"));
85420
+ const bundle = JSON.parse(readFileSync24(path, "utf8"));
85050
85421
  const result = importRoadmapBundle2(bundle, { apply: Boolean(opts.apply) });
85051
85422
  if (globalOpts.json) {
85052
85423
  output(result, true);
@@ -85806,6 +86177,15 @@ function createShadowTodosStorageAdapter(options) {
85806
86177
  mirror.enqueueUpsert("plans", plan, context);
85807
86178
  return plan;
85808
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
+ },
85809
86189
  async delete(id, context) {
85810
86190
  const deleted = await local.plans.delete(id, context);
85811
86191
  if (deleted)