@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/contracts.js CHANGED
@@ -3662,7 +3662,7 @@ function isBlockingDependencyStatus(status) {
3662
3662
  function isTerminalStatus(status) {
3663
3663
  return status === "completed" || status === "failed" || status === "cancelled";
3664
3664
  }
3665
- var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
3665
+ 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;
3666
3666
  var init_types = __esm(() => {
3667
3667
  TASK_STATUSES = [
3668
3668
  "pending",
@@ -3750,6 +3750,19 @@ var init_types = __esm(() => {
3750
3750
  this.name = "ResourceConflictError";
3751
3751
  }
3752
3752
  };
3753
+ PlanRevisionConflictError = class PlanRevisionConflictError extends Error {
3754
+ planId;
3755
+ expectedUpdatedAt;
3756
+ currentUpdatedAt;
3757
+ static code = "PLAN_REVISION_CONFLICT";
3758
+ constructor(planId, expectedUpdatedAt, currentUpdatedAt) {
3759
+ super(`Plan revision conflict for ${planId}: expected ${expectedUpdatedAt}, current ${currentUpdatedAt}`);
3760
+ this.planId = planId;
3761
+ this.expectedUpdatedAt = expectedUpdatedAt;
3762
+ this.currentUpdatedAt = currentUpdatedAt;
3763
+ this.name = "PlanRevisionConflictError";
3764
+ }
3765
+ };
3753
3766
  PlanNotFoundError = class PlanNotFoundError extends Error {
3754
3767
  planId;
3755
3768
  static code = "PLAN_NOT_FOUND";
@@ -10521,6 +10534,48 @@ function updatePlan(id, input, db) {
10521
10534
  return updatePlanStored(id, input, d);
10522
10535
  })();
10523
10536
  }
10537
+ function nextPlanCompletionTimestamp(expectedUpdatedAt) {
10538
+ const expected = Date.parse(expectedUpdatedAt);
10539
+ const minimum = Number.isNaN(expected) ? Date.now() : expected + 2;
10540
+ return new Date(Math.max(Date.now(), minimum)).toISOString();
10541
+ }
10542
+ function completePlanAtRevision(id, expectedUpdatedAt, db) {
10543
+ const d = db || getDatabase();
10544
+ return d.transaction(() => {
10545
+ guardPlanRowsSqlite([id], d);
10546
+ const plan = getPlan(id, d);
10547
+ if (!plan)
10548
+ throw new PlanNotFoundError(id);
10549
+ if (plan.updated_at !== expectedUpdatedAt) {
10550
+ throw new PlanRevisionConflictError(id, expectedUpdatedAt, plan.updated_at);
10551
+ }
10552
+ if (plan.status === "completed")
10553
+ return { plan, applied: false };
10554
+ const updatedAt = nextPlanCompletionTimestamp(expectedUpdatedAt);
10555
+ const result = d.run(`UPDATE plans
10556
+ SET status = 'completed', updated_at = ?
10557
+ WHERE id = ? AND updated_at = ? AND status <> 'completed'`, [updatedAt, id, expectedUpdatedAt]);
10558
+ if (result.changes !== 1) {
10559
+ const current = getPlan(id, d);
10560
+ if (!current)
10561
+ throw new PlanNotFoundError(id);
10562
+ throw new PlanRevisionConflictError(id, expectedUpdatedAt, current.updated_at);
10563
+ }
10564
+ const completed = getPlan(id, d);
10565
+ emitLocalEventHooksQuiet({
10566
+ type: "plan.updated",
10567
+ payload: {
10568
+ id,
10569
+ old_status: plan.status,
10570
+ new_status: completed.status,
10571
+ name: completed.name,
10572
+ project_id: completed.project_id
10573
+ },
10574
+ databasePath: databasePathFromDatabase(d)
10575
+ });
10576
+ return { plan: completed, applied: true };
10577
+ })();
10578
+ }
10524
10579
  function deletePlan(id, db) {
10525
10580
  const d = db || getDatabase();
10526
10581
  const plan = getPlan(id, d);
@@ -12380,7 +12435,7 @@ var init_tasks = __esm(() => {
12380
12435
  // package.json
12381
12436
  var package_default = {
12382
12437
  name: "@hasna/todos",
12383
- version: "0.15.18",
12438
+ version: "0.15.19",
12384
12439
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
12385
12440
  type: "module",
12386
12441
  main: "dist/index.js",
@@ -12,5 +12,9 @@ export declare function createPlan(input: CreatePlanInput, db?: Database): Plan;
12
12
  export declare function getPlan(id: string, db?: Database): Plan | null;
13
13
  export declare function listPlans(projectId?: string, db?: Database): Plan[];
14
14
  export declare function updatePlan(id: string, input: UpdatePlanInput, db?: Database): Plan;
15
+ export declare function completePlanAtRevision(id: string, expectedUpdatedAt: string, db?: Database): {
16
+ plan: Plan;
17
+ applied: boolean;
18
+ };
15
19
  export declare function deletePlan(id: string, db?: Database): boolean;
16
20
  //# sourceMappingURL=plans.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"plans.d.ts","sourceRoot":"","sources":["../../src/db/plans.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAoB,MAAM,YAAY,CAAC;AAC7D,OAAO,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAShF,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,MAAM,EAAE,IAAI,GAAG,MAAM,GAAG,WAAW,GAAG,WAAW,CAAC;IAClD,OAAO,EAAE,IAAI,EAAE,CAAC;CACjB;AAMD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIvD;AAsCD,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,oBAAoB,CAalH;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAEnG;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,IAAI,CA2BtE;AAED,wBAAgB,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,IAAI,GAAG,IAAI,CAI9D;AAED,wBAAgB,SAAS,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,IAAI,EAAE,CAUnE;AAuDD,wBAAgB,UAAU,CACxB,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,eAAe,EACtB,EAAE,CAAC,EAAE,QAAQ,GACZ,IAAI,CAMN;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAW7D"}
1
+ {"version":3,"file":"plans.d.ts","sourceRoot":"","sources":["../../src/db/plans.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAoB,MAAM,YAAY,CAAC;AAC7D,OAAO,KAAK,EAAE,eAAe,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAShF,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAClB,MAAM,EAAE,IAAI,GAAG,MAAM,GAAG,WAAW,GAAG,WAAW,CAAC;IAClD,OAAO,EAAE,IAAI,EAAE,CAAC;CACjB;AAMD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAIvD;AAsCD,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,oBAAoB,CAalH;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,GAAG,IAAI,CAEnG;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,IAAI,CA2BtE;AAED,wBAAgB,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,IAAI,GAAG,IAAI,CAI9D;AAED,wBAAgB,SAAS,CAAC,SAAS,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,IAAI,EAAE,CAUnE;AAuDD,wBAAgB,UAAU,CACxB,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,eAAe,EACtB,EAAE,CAAC,EAAE,QAAQ,GACZ,IAAI,CAMN;AAQD,wBAAgB,sBAAsB,CACpC,EAAE,EAAE,MAAM,EACV,iBAAiB,EAAE,MAAM,EACzB,EAAE,CAAC,EAAE,QAAQ,GACZ;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAqClC;AAED,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,QAAQ,GAAG,OAAO,CAW7D"}
package/dist/index.js CHANGED
@@ -3662,7 +3662,7 @@ function isBlockingDependencyStatus(status) {
3662
3662
  function isTerminalStatus(status) {
3663
3663
  return status === "completed" || status === "failed" || status === "cancelled";
3664
3664
  }
3665
- var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
3665
+ 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;
3666
3666
  var init_types = __esm(() => {
3667
3667
  TASK_STATUSES = [
3668
3668
  "pending",
@@ -3750,6 +3750,19 @@ var init_types = __esm(() => {
3750
3750
  this.name = "ResourceConflictError";
3751
3751
  }
3752
3752
  };
3753
+ PlanRevisionConflictError = class PlanRevisionConflictError extends Error {
3754
+ planId;
3755
+ expectedUpdatedAt;
3756
+ currentUpdatedAt;
3757
+ static code = "PLAN_REVISION_CONFLICT";
3758
+ constructor(planId, expectedUpdatedAt, currentUpdatedAt) {
3759
+ super(`Plan revision conflict for ${planId}: expected ${expectedUpdatedAt}, current ${currentUpdatedAt}`);
3760
+ this.planId = planId;
3761
+ this.expectedUpdatedAt = expectedUpdatedAt;
3762
+ this.currentUpdatedAt = currentUpdatedAt;
3763
+ this.name = "PlanRevisionConflictError";
3764
+ }
3765
+ };
3753
3766
  PlanNotFoundError = class PlanNotFoundError extends Error {
3754
3767
  planId;
3755
3768
  static code = "PLAN_NOT_FOUND";
@@ -10521,6 +10534,48 @@ function updatePlan(id, input, db) {
10521
10534
  return updatePlanStored(id, input, d);
10522
10535
  })();
10523
10536
  }
10537
+ function nextPlanCompletionTimestamp(expectedUpdatedAt) {
10538
+ const expected = Date.parse(expectedUpdatedAt);
10539
+ const minimum = Number.isNaN(expected) ? Date.now() : expected + 2;
10540
+ return new Date(Math.max(Date.now(), minimum)).toISOString();
10541
+ }
10542
+ function completePlanAtRevision(id, expectedUpdatedAt, db) {
10543
+ const d = db || getDatabase();
10544
+ return d.transaction(() => {
10545
+ guardPlanRowsSqlite([id], d);
10546
+ const plan = getPlan(id, d);
10547
+ if (!plan)
10548
+ throw new PlanNotFoundError(id);
10549
+ if (plan.updated_at !== expectedUpdatedAt) {
10550
+ throw new PlanRevisionConflictError(id, expectedUpdatedAt, plan.updated_at);
10551
+ }
10552
+ if (plan.status === "completed")
10553
+ return { plan, applied: false };
10554
+ const updatedAt = nextPlanCompletionTimestamp(expectedUpdatedAt);
10555
+ const result = d.run(`UPDATE plans
10556
+ SET status = 'completed', updated_at = ?
10557
+ WHERE id = ? AND updated_at = ? AND status <> 'completed'`, [updatedAt, id, expectedUpdatedAt]);
10558
+ if (result.changes !== 1) {
10559
+ const current = getPlan(id, d);
10560
+ if (!current)
10561
+ throw new PlanNotFoundError(id);
10562
+ throw new PlanRevisionConflictError(id, expectedUpdatedAt, current.updated_at);
10563
+ }
10564
+ const completed = getPlan(id, d);
10565
+ emitLocalEventHooksQuiet({
10566
+ type: "plan.updated",
10567
+ payload: {
10568
+ id,
10569
+ old_status: plan.status,
10570
+ new_status: completed.status,
10571
+ name: completed.name,
10572
+ project_id: completed.project_id
10573
+ },
10574
+ databasePath: databasePathFromDatabase(d)
10575
+ });
10576
+ return { plan: completed, applied: true };
10577
+ })();
10578
+ }
10524
10579
  function deletePlan(id, db) {
10525
10580
  const d = db || getDatabase();
10526
10581
  const plan = getPlan(id, d);
@@ -12493,7 +12548,7 @@ var init_dispatches = __esm(() => {
12493
12548
  // package.json
12494
12549
  var package_default = {
12495
12550
  name: "@hasna/todos",
12496
- version: "0.15.18",
12551
+ version: "0.15.19",
12497
12552
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
12498
12553
  type: "module",
12499
12554
  main: "dist/index.js",
@@ -25766,7 +25821,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
25766
25821
  }
25767
25822
  if (result.errors.length > 0)
25768
25823
  return result;
25769
- const applyRows = (objectType, table, columns, rows, updateClockColumn, afterUpsert) => {
25824
+ const applyRows = (objectType, table, columns, rows, updateClockColumn, acceptEqualClock = true, afterUpsert) => {
25770
25825
  for (const row of rows) {
25771
25826
  try {
25772
25827
  const record = asRecord2(row);
@@ -25775,7 +25830,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
25775
25830
  result.skipped += 1;
25776
25831
  continue;
25777
25832
  }
25778
- const state = upsertById(d, table, columns, record, updateClockColumn);
25833
+ const state = upsertById(d, table, columns, record, updateClockColumn, acceptEqualClock);
25779
25834
  if (state === "inserted")
25780
25835
  result.inserted += 1;
25781
25836
  else if (state === "updated")
@@ -25792,10 +25847,10 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
25792
25847
  applyRows("project_machine_paths", "project_machine_paths", PROJECT_MACHINE_PATH_COLUMNS, snapshot.projectMachinePaths ?? [], "updated_at");
25793
25848
  applyRows("agents", "agents", AGENT_COLUMNS, snapshot.agents, "last_seen_at");
25794
25849
  applyRows("task_lists", "task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
25795
- applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at");
25850
+ applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at", false);
25796
25851
  applyRows("templates", "task_templates", TEMPLATE_COLUMNS, snapshot.templates);
25797
25852
  applyRows("template_tasks", "template_tasks", TEMPLATE_TASK_COLUMNS, snapshot.templateTasks ?? []);
25798
- applyRows("tasks", "tasks", TASK_COLUMNS, sortedTasks2(snapshot.tasks), "updated_at", (row, changed) => {
25853
+ applyRows("tasks", "tasks", TASK_COLUMNS, sortedTasks2(snapshot.tasks), "updated_at", true, (row, changed) => {
25799
25854
  if (changed && Array.isArray(row["tags"]) && typeof row["id"] === "string") {
25800
25855
  replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
25801
25856
  }
@@ -25804,7 +25859,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
25804
25859
  applyTombstones(d, snapshot.tombstones ?? [], result);
25805
25860
  return result;
25806
25861
  }
25807
- function upsertById(db, table, columns, row, updateClockColumn) {
25862
+ function upsertById(db, table, columns, row, updateClockColumn, acceptEqualClock = true) {
25808
25863
  const id = row["id"];
25809
25864
  if (typeof id !== "string" || !id)
25810
25865
  throw new Error(`${table} row is missing id`);
@@ -25816,7 +25871,7 @@ function upsertById(db, table, columns, row, updateClockColumn) {
25816
25871
  const values = presentColumns.map((column) => valueForColumn(column, row[column]));
25817
25872
  const updateColumns = presentColumns.filter((column) => column !== "id");
25818
25873
  const updateSet = updateColumns.map((column) => column === "version" ? `version = MAX(COALESCE(${table}.version, 0), excluded.version)` : `${column} = excluded.${column}`).join(", ");
25819
- const clockGuard = updateClockColumn && presentColumns.includes(updateClockColumn) ? ` WHERE ${table}.${updateClockColumn} IS NULL OR ${table}.${updateClockColumn} <= excluded.${updateClockColumn}` : "";
25874
+ const clockGuard = updateClockColumn && presentColumns.includes(updateClockColumn) ? ` WHERE ${table}.${updateClockColumn} IS NULL OR ${table}.${updateClockColumn} ${acceptEqualClock ? "<=" : "<"} excluded.${updateClockColumn}` : "";
25820
25875
  const sql = updateSet ? `INSERT INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders})
25821
25876
  ON CONFLICT(id) DO UPDATE SET ${updateSet}${clockGuard}` : `INSERT OR IGNORE INTO ${table} (${presentColumns.join(", ")}) VALUES (${placeholders})`;
25822
25877
  const changes = db.run(sql, values).changes;
@@ -26056,6 +26111,7 @@ function createLocalSqliteTodosStorageAdapter(options = {}) {
26056
26111
  get: (id) => getPlan(id, database()),
26057
26112
  list: (projectId) => listPlans(projectId, database()),
26058
26113
  update: (id, input) => updatePlan(id, input, database()),
26114
+ completeAtRevision: (id, expectedUpdatedAt) => completePlanAtRevision(id, expectedUpdatedAt, database()),
26059
26115
  delete: (id) => deletePlan(id, database())
26060
26116
  },
26061
26117
  planProjectLinks: {
@@ -26600,6 +26656,7 @@ function createPostgresTodosStorageAdapter(options) {
26600
26656
  get: (id) => store.get("plans", id),
26601
26657
  list: async (projectId) => (await store.list("plans")).filter((plan) => projectId === undefined || plan.project_id === projectId).sort((a, b) => a.name.localeCompare(b.name)),
26602
26658
  update: (id, input) => updatePlan2(id, input, store),
26659
+ completeAtRevision: (id, expectedUpdatedAt, context) => store.completePlanAtRevision(id, expectedUpdatedAt, context),
26603
26660
  delete: (id, context) => store.deletePlan(id, context)
26604
26661
  },
26605
26662
  planProjectLinks: {
@@ -27087,6 +27144,54 @@ class PostgresJsonRecordStore {
27087
27144
  throw new PlanNotFoundError(value.id);
27088
27145
  return payloadRecord2(row.payload);
27089
27146
  }
27147
+ async completePlanAtRevision(id, expectedUpdatedAt, context = {}) {
27148
+ await this.ensureSchema();
27149
+ const result = await this.options.client.query(`/* todos:complete-plan-revision-cas */ WITH next_clock AS (
27150
+ SELECT date_trunc(
27151
+ 'milliseconds',
27152
+ GREATEST(clock_timestamp(), ($3::text)::timestamptz + interval '2 milliseconds')
27153
+ ) AS completed_at
27154
+ ), stored AS (
27155
+ UPDATE ${this.tableName} AS record SET
27156
+ payload = record.payload || jsonb_build_object(
27157
+ 'status', 'completed',
27158
+ 'updated_at', to_char(
27159
+ next_clock.completed_at AT TIME ZONE 'UTC',
27160
+ 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"'
27161
+ )
27162
+ ),
27163
+ updated_at = next_clock.completed_at,
27164
+ deleted_at = NULL,
27165
+ source_machine_id = COALESCE($4, record.source_machine_id),
27166
+ version = COALESCE(record.version, 0) + 1
27167
+ FROM next_clock
27168
+ WHERE record.service = $1
27169
+ AND record.object_type = 'plans'
27170
+ AND record.object_id = $2
27171
+ AND record.deleted_at IS NULL
27172
+ AND record.payload->>'updated_at' = $3::text
27173
+ AND record.payload->>'status' IS DISTINCT FROM 'completed'
27174
+ RETURNING record.payload
27175
+ )
27176
+ SELECT payload FROM stored`, [
27177
+ this.service,
27178
+ id,
27179
+ expectedUpdatedAt,
27180
+ context.requestId ?? this.sourceMachineId ?? null
27181
+ ]);
27182
+ const payload = result.rows[0]?.payload;
27183
+ if (payload)
27184
+ return { plan: payloadRecord2(payload), applied: true };
27185
+ const current = await this.get("plans", id);
27186
+ if (!current)
27187
+ throw new PlanNotFoundError(id);
27188
+ if (current.updated_at !== expectedUpdatedAt) {
27189
+ throw new PlanRevisionConflictError(id, expectedUpdatedAt, current.updated_at);
27190
+ }
27191
+ if (current.status === "completed")
27192
+ return { plan: current, applied: false };
27193
+ throw new PlanRevisionConflictError(id, expectedUpdatedAt, current.updated_at);
27194
+ }
27090
27195
  async createTemplateWithTasks(template, tasks, context = {}) {
27091
27196
  await this.ensureSchema();
27092
27197
  const records = [
@@ -28865,6 +28970,15 @@ function createShadowTodosStorageAdapter(options) {
28865
28970
  mirror.enqueueUpsert("plans", plan, context);
28866
28971
  return plan;
28867
28972
  },
28973
+ async completeAtRevision(id, expectedUpdatedAt, context) {
28974
+ if (typeof local.plans.completeAtRevision !== "function") {
28975
+ throw new Error("Atomic plan completion is not supported by the local shadow adapter");
28976
+ }
28977
+ const completed = await local.plans.completeAtRevision(id, expectedUpdatedAt, context);
28978
+ if (completed.applied)
28979
+ mirror.enqueueUpsert("plans", completed.plan, context);
28980
+ return completed;
28981
+ },
28868
28982
  async delete(id, context) {
28869
28983
  const deleted = await local.plans.delete(id, context);
28870
28984
  if (deleted)