@hasna/todos 0.15.28 → 0.15.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/cli/cloud-router.d.ts +15 -0
  2. package/dist/cli/cloud-router.d.ts.map +1 -1
  3. package/dist/cli/commands/project-registration-commands.d.ts +5 -0
  4. package/dist/cli/commands/project-registration-commands.d.ts.map +1 -0
  5. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  6. package/dist/cli/commands/task-manifest-commands.d.ts.map +1 -1
  7. package/dist/cli/index.js +13040 -10368
  8. package/dist/contracts.js +64 -2
  9. package/dist/db/task-crud.d.ts.map +1 -1
  10. package/dist/index.d.ts +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +1489 -149
  13. package/dist/lib/task-parent-integrity.d.ts +14 -0
  14. package/dist/lib/task-parent-integrity.d.ts.map +1 -0
  15. package/dist/mcp/index.js +2073 -140
  16. package/dist/mcp/tools/task-crud.d.ts.map +1 -1
  17. package/dist/mcp.js +1 -1
  18. package/dist/project-registration/adoption-validation.d.ts +3 -0
  19. package/dist/project-registration/adoption-validation.d.ts.map +1 -0
  20. package/dist/project-registration/authority.d.ts +3 -1
  21. package/dist/project-registration/authority.d.ts.map +1 -1
  22. package/dist/project-registration/backend.d.ts +26 -1
  23. package/dist/project-registration/backend.d.ts.map +1 -1
  24. package/dist/project-registration/http.d.ts +3 -1
  25. package/dist/project-registration/http.d.ts.map +1 -1
  26. package/dist/project-registration/index.d.ts +2 -1
  27. package/dist/project-registration/index.d.ts.map +1 -1
  28. package/dist/project-registration/page-validation.d.ts +5 -0
  29. package/dist/project-registration/page-validation.d.ts.map +1 -0
  30. package/dist/project-registration/postgres.d.ts +13 -1
  31. package/dist/project-registration/postgres.d.ts.map +1 -1
  32. package/dist/project-registration/schema.d.ts.map +1 -1
  33. package/dist/project-registration/sqlite.d.ts +13 -1
  34. package/dist/project-registration/sqlite.d.ts.map +1 -1
  35. package/dist/project-registration/types.d.ts +68 -1
  36. package/dist/project-registration/types.d.ts.map +1 -1
  37. package/dist/project-registration.js +873 -63
  38. package/dist/registry.js +64 -2
  39. package/dist/release-provenance.json +5 -5
  40. package/dist/sdk/index.d.ts +1 -1
  41. package/dist/sdk/index.d.ts.map +1 -1
  42. package/dist/sdk/index.js +151 -0
  43. package/dist/sdk/v1.generated.d.ts +282 -0
  44. package/dist/sdk/v1.generated.d.ts.map +1 -1
  45. package/dist/server/index.js +2064 -131
  46. package/dist/server/openapi.d.ts +2856 -1357
  47. package/dist/server/openapi.d.ts.map +1 -1
  48. package/dist/server/v1.d.ts.map +1 -1
  49. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  50. package/dist/storage/postgres-sync.d.ts +8 -0
  51. package/dist/storage/postgres-sync.d.ts.map +1 -1
  52. package/dist/storage.js +244 -19
  53. package/dist/task-manifest/authority.d.ts +13 -1
  54. package/dist/task-manifest/authority.d.ts.map +1 -1
  55. package/dist/task-manifest/backend.d.ts +5 -0
  56. package/dist/task-manifest/backend.d.ts.map +1 -1
  57. package/dist/task-manifest/index.d.ts +2 -2
  58. package/dist/task-manifest/index.d.ts.map +1 -1
  59. package/dist/task-manifest/plan-slug.d.ts +20 -0
  60. package/dist/task-manifest/plan-slug.d.ts.map +1 -1
  61. package/dist/task-manifest/postgres.d.ts +1 -0
  62. package/dist/task-manifest/postgres.d.ts.map +1 -1
  63. package/dist/task-manifest/schema-sql.d.ts.map +1 -1
  64. package/dist/task-manifest/schema.d.ts.map +1 -1
  65. package/dist/task-manifest/sqlite.d.ts +1 -0
  66. package/dist/task-manifest/sqlite.d.ts.map +1 -1
  67. package/dist/task-manifest/types.d.ts +21 -1
  68. package/dist/task-manifest/types.d.ts.map +1 -1
  69. package/dist/task-manifest.js +592 -61
  70. package/dist/types/index.d.ts +4 -2
  71. package/dist/types/index.d.ts.map +1 -1
  72. package/package.json +1 -1
@@ -70,7 +70,7 @@ var package_default;
70
70
  var init_package = __esm(() => {
71
71
  package_default = {
72
72
  name: "@hasna/todos",
73
- version: "0.15.28",
73
+ version: "0.15.32",
74
74
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
75
75
  type: "module",
76
76
  main: "dist/index.js",
@@ -2373,6 +2373,46 @@ var init_integrity = __esm(() => {
2373
2373
  };
2374
2374
  });
2375
2375
 
2376
+ // src/lib/task-parent-integrity.ts
2377
+ function parentCycleError(taskId, parentId) {
2378
+ return new ResourceConflictError("TASK_PARENT_CYCLE", `TASK_PARENT_CYCLE: assigning parent ${parentId} to task ${taskId} would create or retain a parent cycle`);
2379
+ }
2380
+ function assertTaskParentIntegrity(taskId, parentId, getTask) {
2381
+ if (parentId === undefined || parentId === null)
2382
+ return;
2383
+ const visited = new Set;
2384
+ let cursor = parentId;
2385
+ while (cursor) {
2386
+ if (cursor === taskId || visited.has(cursor)) {
2387
+ throw parentCycleError(taskId, parentId);
2388
+ }
2389
+ visited.add(cursor);
2390
+ const parent = getTask(cursor);
2391
+ if (!parent)
2392
+ throw new TaskNotFoundError(cursor);
2393
+ cursor = parent.parent_id;
2394
+ }
2395
+ }
2396
+ async function assertTaskParentIntegrityAsync(taskId, parentId, getTask) {
2397
+ if (parentId === undefined || parentId === null)
2398
+ return;
2399
+ const visited = new Set;
2400
+ let cursor = parentId;
2401
+ while (cursor) {
2402
+ if (cursor === taskId || visited.has(cursor)) {
2403
+ throw parentCycleError(taskId, parentId);
2404
+ }
2405
+ visited.add(cursor);
2406
+ const parent = await getTask(cursor);
2407
+ if (!parent)
2408
+ throw new TaskNotFoundError(cursor);
2409
+ cursor = parent.parent_id;
2410
+ }
2411
+ }
2412
+ var init_task_parent_integrity = __esm(() => {
2413
+ init_types();
2414
+ });
2415
+
2376
2416
  // src/storage/audit-history-import.ts
2377
2417
  function auditHistoryRowsAreFieldIdentical(left, right) {
2378
2418
  return AUDIT_HISTORY_FIELDS.every((field) => {
@@ -2469,8 +2509,8 @@ function createPostgresTodosStorageAdapter(options) {
2469
2509
  resolveRef: (ref) => store.resolveTaskRef(ref),
2470
2510
  list: (filter = {}) => store.listTasks(filter),
2471
2511
  count: (filter = {}) => store.countTasks(filter),
2472
- update: (id, input) => updateTask(id, input, store),
2473
- delete: (id, context) => store.delete("tasks", id, context),
2512
+ update: (id, input, context) => updateTask(id, input, store, context),
2513
+ delete: (id, context) => store.deleteTaskHierarchy(id, context),
2474
2514
  start: (id, agentId) => startTask(id, agentId, store),
2475
2515
  complete: (id, agentId, options2) => completeTask(id, agentId, options2, store),
2476
2516
  fail: (id, agentId, reason, options2) => failTask(id, agentId, reason, options2, store),
@@ -3003,22 +3043,70 @@ class PostgresJsonRecordStore {
3003
3043
  return "identical";
3004
3044
  throw new Error(divergentAuditHistoryReplayError(value.id));
3005
3045
  }
3006
- async upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context = {}) {
3046
+ async withTaskParentIntegrityTransaction(fn) {
3047
+ if (typeof this.options.client.transaction !== "function") {
3048
+ throw new Error("TASK_PARENT_ATOMICITY_UNAVAILABLE: PostgreSQL parent writes and task deletion require transaction(callback)");
3049
+ }
3050
+ return this.options.client.transaction(async (client) => {
3051
+ await client.query("/* todos:task-parent-integrity-lock */ SELECT pg_advisory_xact_lock(hashtextextended($1 || ':task-parent-integrity', 0))", [this.service]);
3052
+ return fn(client);
3053
+ });
3054
+ }
3055
+ async upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context = {}, parentGuard, queryClient) {
3007
3056
  const planIds = [...new Set(guardedPlanIds.filter(Boolean))].sort();
3008
- if (planIds.length === 0)
3057
+ if (planIds.length === 0 && !parentGuard)
3009
3058
  return this.upsert("tasks", value, context);
3010
3059
  await this.ensureSchema();
3060
+ if (parentGuard && !queryClient) {
3061
+ return this.withTaskParentIntegrityTransaction((client2) => this.upsertTaskWithPlanMembershipGuard(value, guardedPlanIds, explicitProject, context, parentGuard, client2));
3062
+ }
3063
+ const client = queryClient ?? this.options.client;
3011
3064
  const updatedAt = value.updated_at;
3012
3065
  const targetPlanId = value.plan_id;
3013
- const result = await this.options.client.query(`/* todos:task-plan-membership-guard */ WITH
3066
+ const result = await client.query(`/* todos:task-plan-membership-guard todos:task-parent-integrity-guard */ WITH RECURSIVE
3067
+ locked_task AS MATERIALIZED (
3068
+ SELECT payload FROM ${this.tableName}
3069
+ WHERE service = $1 AND object_type = 'tasks' AND object_id = $2 AND deleted_at IS NULL
3070
+ FOR UPDATE
3071
+ ),
3014
3072
  locked_plans AS MATERIALIZED (
3015
3073
  SELECT object_id, payload FROM ${this.tableName}
3016
3074
  WHERE service = $1 AND object_type = 'plans' AND deleted_at IS NULL
3017
3075
  AND object_id IN (SELECT value FROM jsonb_array_elements_text($7::jsonb))
3018
3076
  ORDER BY object_id
3019
3077
  FOR UPDATE
3078
+ ), parent_chain(object_id, payload, path, cycle) AS (
3079
+ SELECT parent.object_id, parent.payload, ARRAY[parent.object_id], false
3080
+ FROM ${this.tableName} AS parent
3081
+ WHERE $10::boolean
3082
+ AND $11::text IS NOT NULL
3083
+ AND parent.service = $1
3084
+ AND parent.object_type = 'tasks'
3085
+ AND parent.object_id = $11
3086
+ AND parent.deleted_at IS NULL
3087
+ UNION ALL
3088
+ SELECT ancestor.object_id,
3089
+ ancestor.payload,
3090
+ chain.path || ancestor.object_id,
3091
+ ancestor.object_id = ANY(chain.path)
3092
+ FROM parent_chain AS chain
3093
+ JOIN ${this.tableName} AS ancestor
3094
+ ON ancestor.service = $1
3095
+ AND ancestor.object_type = 'tasks'
3096
+ AND ancestor.object_id = chain.payload->>'parent_id'
3097
+ AND ancestor.deleted_at IS NULL
3098
+ WHERE NOT chain.cycle
3020
3099
  ), validation AS (
3021
3100
  SELECT
3101
+ (NOT $10::boolean OR NOT $13::boolean OR EXISTS (SELECT 1 FROM locked_task)) AS task_found,
3102
+ (NOT $10::boolean OR NOT $13::boolean
3103
+ OR (SELECT (payload->>'version')::integer FROM locked_task) = $12::integer) AS version_matches,
3104
+ (NOT $10::boolean OR $11::text IS NULL
3105
+ OR EXISTS (SELECT 1 FROM parent_chain WHERE object_id = $11)) AS parent_found,
3106
+ (NOT $10::boolean OR $11::text IS NULL
3107
+ OR ($11::text <> $2
3108
+ AND NOT EXISTS (SELECT 1 FROM parent_chain WHERE object_id = $2)
3109
+ AND NOT EXISTS (SELECT 1 FROM parent_chain WHERE cycle))) AS parent_acyclic,
3022
3110
  (SELECT count(*) FROM locked_plans) = jsonb_array_length($7::jsonb) AS all_plans_found,
3023
3111
  ($8::text IS NULL OR EXISTS (SELECT 1 FROM locked_plans WHERE object_id = $8)) AS target_plan_found,
3024
3112
  (SELECT payload->>'project_id' FROM locked_plans WHERE object_id = $8) AS target_project_id
@@ -3039,7 +3127,13 @@ class PostgresJsonRecordStore {
3039
3127
  )
3040
3128
  SELECT $1, 'tasks', $2, guarded.payload, $4::timestamptz, NULL, $5, $6
3041
3129
  FROM guarded
3042
- WHERE guarded.all_plans_found AND guarded.target_plan_found AND NOT guarded.project_conflict
3130
+ WHERE guarded.task_found
3131
+ AND guarded.version_matches
3132
+ AND guarded.parent_found
3133
+ AND guarded.parent_acyclic
3134
+ AND guarded.all_plans_found
3135
+ AND guarded.target_plan_found
3136
+ AND NOT guarded.project_conflict
3043
3137
  ON CONFLICT (service, object_type, object_id) DO UPDATE SET
3044
3138
  payload = EXCLUDED.payload,
3045
3139
  updated_at = EXCLUDED.updated_at,
@@ -3052,8 +3146,10 @@ class PostgresJsonRecordStore {
3052
3146
  AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))
3053
3147
  RETURNING payload
3054
3148
  )
3055
- SELECT guarded.all_plans_found, guarded.target_plan_found, guarded.project_conflict,
3056
- (SELECT payload FROM stored) AS payload
3149
+ SELECT guarded.task_found, guarded.version_matches, guarded.parent_found, guarded.parent_acyclic,
3150
+ guarded.all_plans_found, guarded.target_plan_found, guarded.project_conflict,
3151
+ (SELECT payload FROM stored) AS payload,
3152
+ (SELECT payload FROM locked_task) AS current_payload
3057
3153
  FROM guarded`, [
3058
3154
  this.service,
3059
3155
  value.id,
@@ -3063,9 +3159,26 @@ class PostgresJsonRecordStore {
3063
3159
  numberValue2(value.version),
3064
3160
  jsonbParam(planIds),
3065
3161
  targetPlanId,
3066
- explicitProject
3162
+ explicitProject,
3163
+ Boolean(parentGuard),
3164
+ parentGuard?.parentId ?? null,
3165
+ parentGuard?.expectedVersion ?? null,
3166
+ parentGuard?.operation === "update"
3067
3167
  ]);
3068
3168
  const row = result.rows[0];
3169
+ if (parentGuard && !row?.task_found) {
3170
+ throw new TaskNotFoundError(value.id);
3171
+ }
3172
+ if (parentGuard && !row?.version_matches) {
3173
+ const current = row?.current_payload ? payloadRecord2(row.current_payload) : await this.get("tasks", value.id);
3174
+ throw new VersionConflictError(value.id, parentGuard.expectedVersion, current?.version ?? -1);
3175
+ }
3176
+ if (parentGuard && !row?.parent_found && parentGuard.parentId) {
3177
+ throw new TaskNotFoundError(parentGuard.parentId);
3178
+ }
3179
+ if (parentGuard && !row?.parent_acyclic && parentGuard.parentId) {
3180
+ throw new ResourceConflictError("TASK_PARENT_CYCLE", `TASK_PARENT_CYCLE: assigning parent ${parentGuard.parentId} to task ${value.id} would create or retain a parent cycle`);
3181
+ }
3069
3182
  if (!row?.all_plans_found || !row.target_plan_found) {
3070
3183
  throw new PlanProjectLinkError("PLAN_PROJECT_LINK_PLAN_NOT_FOUND", `Plan membership changed through a missing plan: ${targetPlanId ?? planIds.join(", ")}`, { plan_ids: planIds, target_plan_id: targetPlanId });
3071
3184
  }
@@ -3073,6 +3186,8 @@ class PostgresJsonRecordStore {
3073
3186
  throw new ResourceConflictError("PLAN_PROJECT_LINK_CONFLICT", `Task project conflicts with linked plan ${targetPlanId}`);
3074
3187
  }
3075
3188
  if (!row.payload) {
3189
+ if (row.current_payload)
3190
+ return payloadRecord2(row.current_payload);
3076
3191
  return await requireRecord("tasks", value.id, this);
3077
3192
  }
3078
3193
  return payloadRecord2(row.payload);
@@ -3398,6 +3513,80 @@ class PostgresJsonRecordStore {
3398
3513
  version: numberValue2(existing["version"])
3399
3514
  }, context);
3400
3515
  }
3516
+ async deleteTaskHierarchy(id, context = {}) {
3517
+ await this.ensureSchema();
3518
+ return this.withTaskParentIntegrityTransaction(async (client) => {
3519
+ const timestamp = new Date().toISOString();
3520
+ const result = await client.query(`/* todos:task-parent-integrity-delete */ WITH RECURSIVE
3521
+ task_tree(object_id, path, cycle) AS (
3522
+ SELECT task.object_id, ARRAY[task.object_id], false
3523
+ FROM ${this.tableName} AS task
3524
+ WHERE task.service = $1
3525
+ AND task.object_type = 'tasks'
3526
+ AND task.object_id = $2
3527
+ AND task.deleted_at IS NULL
3528
+ UNION ALL
3529
+ SELECT child.object_id,
3530
+ tree.path || child.object_id,
3531
+ child.object_id = ANY(tree.path)
3532
+ FROM task_tree AS tree
3533
+ JOIN ${this.tableName} AS child
3534
+ ON child.service = $1
3535
+ AND child.object_type = 'tasks'
3536
+ AND child.payload->>'parent_id' = tree.object_id
3537
+ AND child.deleted_at IS NULL
3538
+ WHERE NOT tree.cycle
3539
+ ), tombstoned AS (
3540
+ UPDATE ${this.tableName} AS task
3541
+ SET deleted_at = $3::timestamptz,
3542
+ updated_at = $3::timestamptz,
3543
+ source_machine_id = $4
3544
+ WHERE task.service = $1
3545
+ AND task.object_type = 'tasks'
3546
+ AND task.deleted_at IS NULL
3547
+ AND task.object_id IN (
3548
+ SELECT object_id FROM task_tree WHERE NOT cycle
3549
+ )
3550
+ RETURNING task.object_id
3551
+ ), tombstoned_related AS (
3552
+ UPDATE ${this.tableName} AS related
3553
+ SET deleted_at = $3::timestamptz,
3554
+ updated_at = $3::timestamptz,
3555
+ source_machine_id = $4
3556
+ WHERE related.service = $1
3557
+ AND related.deleted_at IS NULL
3558
+ AND (
3559
+ (
3560
+ related.object_type = 'dependencies'
3561
+ AND (
3562
+ related.payload->>'task_id' IN (
3563
+ SELECT object_id FROM task_tree WHERE NOT cycle
3564
+ )
3565
+ OR related.payload->>'depends_on' IN (
3566
+ SELECT object_id FROM task_tree WHERE NOT cycle
3567
+ )
3568
+ )
3569
+ )
3570
+ OR (
3571
+ related.object_type IN ('comments', 'verifications', 'commits', 'refs')
3572
+ AND related.payload->>'task_id' IN (
3573
+ SELECT object_id FROM task_tree WHERE NOT cycle
3574
+ )
3575
+ )
3576
+ )
3577
+ RETURNING related.object_id
3578
+ )
3579
+ SELECT EXISTS (SELECT 1 FROM task_tree WHERE object_id = $2) AS found,
3580
+ (SELECT count(*) FROM tombstoned) AS deleted_count,
3581
+ (SELECT count(*) FROM tombstoned_related) AS related_deleted_count`, [
3582
+ this.service,
3583
+ id,
3584
+ timestamp,
3585
+ context.requestId ?? this.sourceMachineId ?? null
3586
+ ]);
3587
+ return Boolean(result.rows[0]?.found);
3588
+ });
3589
+ }
3401
3590
  async getPlanProjectLinkReceipt(receiptId) {
3402
3591
  const value = await this.get("plan_project_link_receipts", receiptId);
3403
3592
  return value ? assertPlanProjectLinkReceipt(value) : null;
@@ -3818,9 +4007,8 @@ class PostgresJsonRecordStore {
3818
4007
  }
3819
4008
  async function createTask(input, store, context) {
3820
4009
  const timestamp = new Date().toISOString();
3821
- if (input.parent_id && !await store.get("tasks", input.parent_id)) {
3822
- throw new TaskNotFoundError(input.parent_id);
3823
- }
4010
+ const taskId = randomUUID();
4011
+ await assertTaskParentIntegrityAsync(taskId, input.parent_id, (id) => store.get("tasks", id));
3824
4012
  const linkedPlan = input.plan_id ? await store.get("plans", input.plan_id) : null;
3825
4013
  const requestedProjectId = input.project_id ?? context?.projectId ?? null;
3826
4014
  if (linkedPlan?.project_id && requestedProjectId && requestedProjectId !== linkedPlan.project_id) {
@@ -3829,7 +4017,7 @@ async function createTask(input, store, context) {
3829
4017
  const effectiveProjectId = linkedPlan?.project_id ?? requestedProjectId;
3830
4018
  const shortId = effectiveProjectId ? await nextTaskShortId(effectiveProjectId, store, context) : null;
3831
4019
  const task = {
3832
- id: randomUUID(),
4020
+ id: taskId,
3833
4021
  short_id: shortId,
3834
4022
  project_id: effectiveProjectId,
3835
4023
  parent_id: input.parent_id ?? null,
@@ -3885,15 +4073,16 @@ async function createTask(input, store, context) {
3885
4073
  synced_at: null,
3886
4074
  archived_at: null
3887
4075
  };
3888
- const storedTask = await store.upsertTaskWithPlanMembershipGuard(task, task.plan_id ? [task.plan_id] : [], input.project_id !== undefined || context?.projectId !== undefined, context);
4076
+ const storedTask = await store.upsertTaskWithPlanMembershipGuard(task, task.plan_id ? [task.plan_id] : [], input.project_id !== undefined || context?.projectId !== undefined, context, input.parent_id ? { operation: "create", expectedVersion: 0, parentId: input.parent_id } : undefined);
3889
4077
  await logTaskChange(storedTask.id, "created", "status", null, storedTask.status, storedTask.assigned_by ?? storedTask.agent_id, store, context);
3890
4078
  return storedTask;
3891
4079
  }
3892
- async function updateTask(id, input, store) {
4080
+ async function updateTask(id, input, store, context) {
3893
4081
  const existing = await requireRecord("tasks", id, store);
3894
4082
  if (existing.version !== input.version) {
3895
- throw new Error(`Task ${id} version conflict: expected ${existing.version}, got ${input.version}`);
4083
+ throw new VersionConflictError(id, input.version, existing.version);
3896
4084
  }
4085
+ await assertTaskParentIntegrityAsync(id, input.parent_id, (candidateId) => store.get("tasks", candidateId));
3897
4086
  const effectivePlanId = input.plan_id !== undefined ? input.plan_id : existing.plan_id;
3898
4087
  const linkedPlan = effectivePlanId ? await store.get("plans", effectivePlanId) : null;
3899
4088
  if (linkedPlan?.project_id) {
@@ -3918,10 +4107,19 @@ async function updateTask(id, input, store) {
3918
4107
  metadata: input.metadata ?? existing.metadata,
3919
4108
  requires_approval: input.requires_approval ?? existing.requires_approval,
3920
4109
  task_list_id: input.task_list_id !== undefined ? input.task_list_id : existing.task_list_id,
4110
+ parent_id: input.parent_id !== undefined ? input.parent_id : existing.parent_id,
3921
4111
  created_by: existing.created_by,
3922
4112
  completed_at: reopened ? null : input.completed_at !== undefined ? input.completed_at : existing.completed_at
3923
4113
  };
3924
- return store.upsertTaskWithPlanMembershipGuard(task, [existing.plan_id, effectivePlanId].filter((planId) => Boolean(planId)), input.project_id !== undefined);
4114
+ const storedTask = await store.upsertTaskWithPlanMembershipGuard(task, [existing.plan_id, effectivePlanId].filter((planId) => Boolean(planId)), input.project_id !== undefined, context, {
4115
+ operation: "update",
4116
+ expectedVersion: input.version,
4117
+ parentId: input.parent_id !== undefined ? input.parent_id : existing.parent_id
4118
+ });
4119
+ if (input.parent_id !== undefined && input.parent_id !== existing.parent_id) {
4120
+ await logTaskChange(id, "update", "parent_id", existing.parent_id, input.parent_id, existing.assigned_to ?? existing.agent_id, store, context);
4121
+ }
4122
+ return storedTask;
3925
4123
  }
3926
4124
  async function startTask(id, agentId, store) {
3927
4125
  const task = await requireRecord("tasks", id, store);
@@ -3985,7 +4183,11 @@ async function patchTask(task, patch, store) {
3985
4183
  version: task.version + 1,
3986
4184
  updated_at: new Date().toISOString()
3987
4185
  };
3988
- return store.upsertTaskWithPlanMembershipGuard(updated, [task.plan_id, updated.plan_id].filter((planId) => Boolean(planId)), Object.prototype.hasOwnProperty.call(patch, "project_id"));
4186
+ return store.upsertTaskWithPlanMembershipGuard(updated, [task.plan_id, updated.plan_id].filter((planId) => Boolean(planId)), Object.prototype.hasOwnProperty.call(patch, "project_id"), {}, {
4187
+ operation: "update",
4188
+ expectedVersion: task.version,
4189
+ parentId: updated.parent_id
4190
+ });
3989
4191
  }
3990
4192
  function sameCloudLockHolder(stored, incoming) {
3991
4193
  if (!stored || !incoming)
@@ -4688,6 +4890,7 @@ var init_postgres_adapter = __esm(() => {
4688
4890
  init_postgres_sync();
4689
4891
  init_integrity();
4690
4892
  init_redaction();
4893
+ init_task_parent_integrity();
4691
4894
  init_audit_history_import();
4692
4895
  init_canonical();
4693
4896
  TASK_ORDER_BY = `ORDER BY ${TASK_ORDER_TIEBREAK}`;
@@ -6612,6 +6815,12 @@ function sqliteTodosProjectRegistrationSchemaSql() {
6612
6815
  authority_id, tenant_id, corpus_id, operation_id, step_id,
6613
6816
  resource_kind, direction, idempotency_key
6614
6817
  );
6818
+ CREATE INDEX IF NOT EXISTS idx_todos_project_registration_receipts_source_identity
6819
+ ON todos_project_registration_receipts (
6820
+ authority_id, tenant_id, corpus_id, route, package_version,
6821
+ operation_id, step_id, resource_kind, direction, idempotency_key,
6822
+ target_selector
6823
+ );
6615
6824
  CREATE INDEX IF NOT EXISTS idx_todos_project_registration_receipts_step
6616
6825
  ON todos_project_registration_receipts (
6617
6826
  authority_id, tenant_id, corpus_id, operation_id, step_id,
@@ -6711,6 +6920,12 @@ function postgresTodosProjectRegistrationSchemaSql() {
6711
6920
  authority_id, tenant_id, corpus_id, operation_id, step_id,
6712
6921
  resource_kind, direction, idempotency_key
6713
6922
  )`,
6923
+ `CREATE INDEX IF NOT EXISTS todos_project_registration_receipts_source_identity_idx
6924
+ ON todos_project_registration_receipts (
6925
+ authority_id, tenant_id, corpus_id, route, package_version,
6926
+ operation_id, step_id, resource_kind, direction, idempotency_key,
6927
+ target_selector
6928
+ )`,
6714
6929
  `CREATE INDEX IF NOT EXISTS todos_project_registration_receipts_step_idx
6715
6930
  ON todos_project_registration_receipts (
6716
6931
  authority_id, tenant_id, corpus_id, operation_id, step_id,
@@ -6852,8 +7067,9 @@ class PostgresTodosProjectRegistrationTransaction {
6852
7067
  const result = await this.client.query(`
6853
7068
  SELECT * FROM todos_project_registration_receipts
6854
7069
  WHERE authority_id = $1 AND tenant_id = $2 AND corpus_id = $3
6855
- AND operation_id = $4 AND step_id = $5 AND resource_kind = $6
6856
- AND direction = $7 AND idempotency_key = $8 AND target_selector = $9
7070
+ AND route = $4 AND package_version = $5
7071
+ AND operation_id = $6 AND step_id = $7 AND resource_kind = $8
7072
+ AND direction = $9 AND idempotency_key = $10 AND target_selector = $11
6857
7073
  ORDER BY CASE outcome
6858
7074
  WHEN 'terminal_nonacceptance' THEN 0
6859
7075
  WHEN 'duplicate_of_accepted' THEN 1
@@ -6864,6 +7080,8 @@ class PostgresTodosProjectRegistrationTransaction {
6864
7080
  identity.authority_id,
6865
7081
  identity.tenant_id,
6866
7082
  identity.corpus_id,
7083
+ identity.route,
7084
+ identity.package_version,
6867
7085
  identity.operation_id,
6868
7086
  identity.step_id,
6869
7087
  identity.resource_kind,
@@ -7063,6 +7281,7 @@ class PostgresTodosProjectRegistrationTransaction {
7063
7281
  AND (payload->>'path' = $2 OR payload->>'task_list_id' = $3)
7064
7282
  ORDER BY payload->>'created_at' ASC, object_id ASC
7065
7283
  LIMIT 1
7284
+ FOR UPDATE
7066
7285
  `, [this.service, path, taskListSlug]);
7067
7286
  return result.rows[0] ? parsePayload(result.rows[0].payload) : null;
7068
7287
  }
@@ -7073,6 +7292,7 @@ class PostgresTodosProjectRegistrationTransaction {
7073
7292
  AND payload->>'project_id' = $2 AND payload->>'slug' = $3
7074
7293
  ORDER BY payload->>'created_at' ASC, object_id ASC
7075
7294
  LIMIT 1
7295
+ FOR UPDATE
7076
7296
  `, [this.service, projectId, slug]);
7077
7297
  return result.rows[0] ? parsePayload(result.rows[0].payload) : null;
7078
7298
  }
@@ -7083,10 +7303,24 @@ class PostgresTodosProjectRegistrationTransaction {
7083
7303
  return await this.storage.taskLists.create(input);
7084
7304
  }
7085
7305
  async getProject(id) {
7086
- return await this.storage.projects.get(id);
7306
+ const result = await this.client.query(`
7307
+ SELECT payload FROM ${this.tableName}
7308
+ WHERE service = $1 AND object_type = 'projects' AND object_id = $2
7309
+ AND deleted_at IS NULL
7310
+ LIMIT 1
7311
+ FOR SHARE
7312
+ `, [this.service, id]);
7313
+ return result.rows[0] ? parsePayload(result.rows[0].payload) : null;
7087
7314
  }
7088
7315
  async getTaskList(id) {
7089
- return await this.storage.taskLists.get(id);
7316
+ const result = await this.client.query(`
7317
+ SELECT payload FROM ${this.tableName}
7318
+ WHERE service = $1 AND object_type = 'task_lists' AND object_id = $2
7319
+ AND deleted_at IS NULL
7320
+ LIMIT 1
7321
+ FOR SHARE
7322
+ `, [this.service, id]);
7323
+ return result.rows[0] ? parsePayload(result.rows[0].payload) : null;
7090
7324
  }
7091
7325
  async lockCompensationWrites() {
7092
7326
  await this.client.query(`LOCK TABLE ${this.tableName} IN SHARE ROW EXCLUSIVE MODE`);
@@ -7166,6 +7400,96 @@ class PostgresTodosProjectRegistrationBackend {
7166
7400
  async getTaskList(id) {
7167
7401
  return (await this.direct()).getTaskList(id);
7168
7402
  }
7403
+ async getProjectResourceCollectionRevision(input) {
7404
+ await this.ensureSchema();
7405
+ const result = await this.client.query(`
7406
+ WITH resources(kind_rank, target_id, revision) AS (
7407
+ SELECT 0, object_id, COALESCE(payload->>'updated_at', updated_at::text)
7408
+ FROM ${this.tableName}
7409
+ WHERE service = $1 AND object_type = 'projects'
7410
+ AND deleted_at IS NULL AND object_id = $2
7411
+ UNION ALL
7412
+ SELECT 1, object_id, COALESCE(payload->>'updated_at', updated_at::text)
7413
+ FROM ${this.tableName}
7414
+ WHERE service = $1 AND object_type = 'task_lists'
7415
+ AND deleted_at IS NULL AND object_id = $3
7416
+ AND payload->>'project_id' = $2
7417
+ UNION ALL
7418
+ SELECT 2, object_id, COALESCE(payload->>'updated_at', updated_at::text)
7419
+ FROM ${this.tableName}
7420
+ WHERE $4::boolean AND service = $1 AND object_type = 'plans'
7421
+ AND deleted_at IS NULL AND payload->>'project_id' = $2
7422
+ UNION ALL
7423
+ SELECT 3, object_id, COALESCE(payload->>'updated_at', updated_at::text)
7424
+ FROM ${this.tableName}
7425
+ WHERE $4::boolean AND service = $1 AND object_type = 'tasks'
7426
+ AND deleted_at IS NULL AND payload->>'project_id' = $2
7427
+ )
7428
+ SELECT 'md5:' || md5(COALESCE(string_agg(
7429
+ kind_rank::text || chr(31) || target_id || chr(31) || revision,
7430
+ chr(30) ORDER BY kind_rank ASC, target_id ASC
7431
+ ), '')) AS revision
7432
+ FROM resources
7433
+ `, [
7434
+ this.service,
7435
+ input.todos_project_id,
7436
+ input.task_list_id,
7437
+ input.include_anchors
7438
+ ]);
7439
+ const revision = result.rows[0]?.revision;
7440
+ if (!revision) {
7441
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_RECORD_NOT_FOUND", "could not derive the hosted project-resource collection revision");
7442
+ }
7443
+ return revision;
7444
+ }
7445
+ async listProjectResourceCandidates(input) {
7446
+ await this.ensureSchema();
7447
+ const afterRank = input.after?.kind_rank ?? -1;
7448
+ const afterId = input.after?.target_id ?? "";
7449
+ const result = await this.client.query(`
7450
+ WITH resources(kind, kind_rank, target_id, parent_id, revision) AS (
7451
+ SELECT 'project'::text, 0, object_id, NULL::text,
7452
+ COALESCE(payload->>'updated_at', updated_at::text)
7453
+ FROM ${this.tableName}
7454
+ WHERE service = $1 AND object_type = 'projects'
7455
+ AND deleted_at IS NULL AND object_id = $2
7456
+ UNION ALL
7457
+ SELECT 'task_list'::text, 1, object_id, payload->>'project_id',
7458
+ COALESCE(payload->>'updated_at', updated_at::text)
7459
+ FROM ${this.tableName}
7460
+ WHERE service = $1 AND object_type = 'task_lists'
7461
+ AND deleted_at IS NULL AND object_id = $3
7462
+ AND payload->>'project_id' = $2
7463
+ UNION ALL
7464
+ SELECT 'plan'::text, 2, object_id, payload->>'project_id',
7465
+ COALESCE(payload->>'updated_at', updated_at::text)
7466
+ FROM ${this.tableName}
7467
+ WHERE $4::boolean AND service = $1 AND object_type = 'plans'
7468
+ AND deleted_at IS NULL AND payload->>'project_id' = $2
7469
+ UNION ALL
7470
+ SELECT 'task'::text, 3, object_id,
7471
+ COALESCE(payload->>'plan_id', payload->>'project_id'),
7472
+ COALESCE(payload->>'updated_at', updated_at::text)
7473
+ FROM ${this.tableName}
7474
+ WHERE $4::boolean AND service = $1 AND object_type = 'tasks'
7475
+ AND deleted_at IS NULL AND payload->>'project_id' = $2
7476
+ )
7477
+ SELECT kind, kind_rank, target_id, parent_id, revision
7478
+ FROM resources
7479
+ WHERE kind_rank > $5 OR (kind_rank = $5 AND target_id > $6)
7480
+ ORDER BY kind_rank ASC, target_id ASC
7481
+ LIMIT $7
7482
+ `, [
7483
+ this.service,
7484
+ input.todos_project_id,
7485
+ input.task_list_id,
7486
+ input.include_anchors,
7487
+ afterRank,
7488
+ afterId,
7489
+ input.limit
7490
+ ]);
7491
+ return result.rows;
7492
+ }
7169
7493
  }
7170
7494
  var init_postgres2 = __esm(() => {
7171
7495
  init_postgres_adapter();
@@ -15289,6 +15613,7 @@ function createTaskStored(input, d) {
15289
15613
  let id = uuid();
15290
15614
  for (let attempt = 0;attempt < 3; attempt++) {
15291
15615
  try {
15616
+ assertTaskParentIntegrity(id, input.parent_id, (candidateId) => getTask(candidateId, d));
15292
15617
  d.run(`INSERT INTO tasks (id, short_id, project_id, parent_id, plan_id, task_list_id, cycle_id, title, description, status, priority, agent_id, assigned_to, session_id, working_dir, tags, metadata, version, created_at, updated_at, due_at, estimated_minutes, sla_minutes, confidence, retry_count, max_retries, retry_after, requires_approval, approved_by, approved_at, recurrence_rule, recurrence_parent_id, spawns_template_id, reason, spawned_from_session, assigned_by, created_by, assigned_from_project, task_type, machine_id)
15293
15618
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
15294
15619
  id,
@@ -15667,6 +15992,7 @@ function updateTaskStored(id, input, db) {
15667
15992
  throw new VersionConflictError(id, input.version, task.version);
15668
15993
  }
15669
15994
  input = sanitizeUpdateTaskInput(input);
15995
+ assertTaskParentIntegrity(id, input.parent_id, (candidateId) => getTask(candidateId, d));
15670
15996
  const effectivePlanId = input.plan_id !== undefined ? input.plan_id : task.plan_id;
15671
15997
  const linkedProjectId = linkedPlanProjectId(effectivePlanId, d);
15672
15998
  if (linkedProjectId) {
@@ -15720,6 +16046,10 @@ function updateTaskStored(id, input, db) {
15720
16046
  sets.push("project_id = ?");
15721
16047
  params.push(input.project_id);
15722
16048
  }
16049
+ if (input.parent_id !== undefined) {
16050
+ sets.push("parent_id = ?");
16051
+ params.push(input.parent_id);
16052
+ }
15723
16053
  if (input.assigned_to !== undefined) {
15724
16054
  sets.push("assigned_to = ?");
15725
16055
  params.push(input.assigned_to);
@@ -15835,6 +16165,8 @@ function updateTaskStored(id, input, db) {
15835
16165
  logTaskChange2(id, "update", "priority", task.priority, input.priority, agentId, d);
15836
16166
  if (input.title !== undefined && input.title !== task.title)
15837
16167
  logTaskChange2(id, "update", "title", task.title, input.title, agentId, d);
16168
+ if (input.parent_id !== undefined && input.parent_id !== task.parent_id)
16169
+ logTaskChange2(id, "update", "parent_id", task.parent_id, input.parent_id, agentId, d);
15838
16170
  if (input.assigned_to !== undefined && input.assigned_to !== task.assigned_to)
15839
16171
  logTaskChange2(id, "update", "assigned_to", task.assigned_to, input.assigned_to, agentId, d);
15840
16172
  if (input.working_dir !== undefined && input.working_dir !== task.working_dir)
@@ -15892,7 +16224,8 @@ function updateTask2(id, input, db) {
15892
16224
  if (!before)
15893
16225
  throw new TaskNotFoundError(id);
15894
16226
  const guardedPlanIds = [before.plan_id, input.plan_id];
15895
- if (!guardedPlanIds.some(Boolean))
16227
+ const needsSerializedWrite = input.parent_id !== undefined || guardedPlanIds.some(Boolean);
16228
+ if (!needsSerializedWrite)
15896
16229
  return updateTaskStored(id, input, d);
15897
16230
  return d.transaction(() => {
15898
16231
  guardPlanRowsSqlite(guardedPlanIds, d);
@@ -15928,6 +16261,7 @@ var init_task_crud = __esm(() => {
15928
16261
  init_checklists();
15929
16262
  init_storage_tombstones();
15930
16263
  init_prewrite_secrets();
16264
+ init_task_parent_integrity();
15931
16265
  });
15932
16266
 
15933
16267
  // src/db/task-status.ts
@@ -20463,6 +20797,7 @@ class SqliteTodosProjectRegistrationTransaction {
20463
20797
  const row = this.db.query(`
20464
20798
  SELECT * FROM todos_project_registration_receipts
20465
20799
  WHERE authority_id = ? AND tenant_id = ? AND corpus_id = ?
20800
+ AND route = ? AND package_version = ?
20466
20801
  AND operation_id = ? AND step_id = ? AND resource_kind = ?
20467
20802
  AND direction = ? AND idempotency_key = ? AND target_selector = ?
20468
20803
  ORDER BY CASE outcome
@@ -20471,7 +20806,7 @@ class SqliteTodosProjectRegistrationTransaction {
20471
20806
  ELSE 2
20472
20807
  END, created_at DESC, receipt_id DESC
20473
20808
  LIMIT 1
20474
- `).get(identity.authority_id, identity.tenant_id, identity.corpus_id, identity.operation_id, identity.step_id, identity.resource_kind, identity.direction, identity.idempotency_key, identity.target_selector);
20809
+ `).get(identity.authority_id, identity.tenant_id, identity.corpus_id, identity.route, identity.package_version, identity.operation_id, identity.step_id, identity.resource_kind, identity.direction, identity.idempotency_key, identity.target_selector);
20475
20810
  return row ? receiptFromRow2(row) : null;
20476
20811
  }
20477
20812
  async getReceiptById(receiptId) {
@@ -20625,7 +20960,7 @@ class StagedSqliteTodosProjectRegistrationTransaction {
20625
20960
  }
20626
20961
  async lockStep(_identity) {}
20627
20962
  async getReceiptForLookup(identity) {
20628
- const staged = [...this.receipts.values()].filter((receipt) => receipt.authority_id === identity.authority_id && receipt.tenant_id === identity.tenant_id && receipt.corpus_id === identity.corpus_id && receipt.operation_id === identity.operation_id && receipt.step_id === identity.step_id && receipt.resource_kind === identity.resource_kind && receipt.direction === identity.direction && receipt.idempotency_key === identity.idempotency_key && receipt.target_selector === identity.target_selector);
20963
+ const staged = [...this.receipts.values()].filter((receipt) => receipt.authority_id === identity.authority_id && receipt.tenant_id === identity.tenant_id && receipt.corpus_id === identity.corpus_id && receipt.route === identity.route && receipt.package_version === identity.package_version && receipt.operation_id === identity.operation_id && receipt.step_id === identity.step_id && receipt.resource_kind === identity.resource_kind && receipt.direction === identity.direction && receipt.idempotency_key === identity.idempotency_key && receipt.target_selector === identity.target_selector);
20629
20964
  const stored = await this.direct.getReceiptForLookup(identity);
20630
20965
  if (stored)
20631
20966
  staged.push(stored);
@@ -21113,35 +21448,68 @@ function projectRecord(project) {
21113
21448
  return {
21114
21449
  target_id: project.id,
21115
21450
  revision: project.updated_at,
21116
- digest: digestProjectRegistrationValue({
21117
- id: project.id,
21118
- name: project.name,
21119
- path: project.path,
21120
- description: project.description,
21121
- task_list_id: project.task_list_id,
21122
- task_prefix: project.task_prefix,
21123
- task_counter: project.task_counter,
21124
- created_at: project.created_at,
21125
- updated_at: project.updated_at
21126
- })
21451
+ digest: projectRegistrationDigest(project)
21127
21452
  };
21128
21453
  }
21129
21454
  function taskListRecord(taskList) {
21130
21455
  return {
21131
21456
  target_id: taskList.id,
21132
21457
  revision: taskList.updated_at,
21133
- digest: digestProjectRegistrationValue({
21134
- id: taskList.id,
21135
- project_id: taskList.project_id,
21136
- slug: taskList.slug,
21137
- name: taskList.name,
21138
- description: taskList.description,
21139
- metadata: taskList.metadata,
21140
- created_at: taskList.created_at,
21141
- updated_at: taskList.updated_at
21458
+ digest: taskListRegistrationDigest(taskList)
21459
+ };
21460
+ }
21461
+ function boundExistingProjectRecord(project) {
21462
+ return {
21463
+ target_id: project.id,
21464
+ revision: project.created_at,
21465
+ digest: projectRegistrationDigest({
21466
+ ...project,
21467
+ updated_at: project.created_at
21142
21468
  })
21143
21469
  };
21144
21470
  }
21471
+ function boundExistingTaskListRecord(taskList) {
21472
+ return {
21473
+ target_id: taskList.id,
21474
+ revision: taskList.created_at,
21475
+ digest: taskListRegistrationDigest({
21476
+ ...taskList,
21477
+ updated_at: taskList.created_at
21478
+ })
21479
+ };
21480
+ }
21481
+ function projectRegistrationDigest(project) {
21482
+ return digestProjectRegistrationValue({
21483
+ id: project.id,
21484
+ name: project.name,
21485
+ path: project.path,
21486
+ description: project.description,
21487
+ task_list_id: project.task_list_id,
21488
+ task_prefix: project.task_prefix,
21489
+ task_counter: project.task_counter,
21490
+ created_at: project.created_at,
21491
+ updated_at: project.updated_at
21492
+ });
21493
+ }
21494
+ function taskListRegistrationDigest(taskList) {
21495
+ return digestProjectRegistrationValue({
21496
+ id: taskList.id,
21497
+ project_id: taskList.project_id,
21498
+ slug: taskList.slug,
21499
+ name: taskList.name,
21500
+ description: taskList.description,
21501
+ metadata: taskList.metadata,
21502
+ created_at: taskList.created_at,
21503
+ updated_at: taskList.updated_at
21504
+ });
21505
+ }
21506
+ function canonicalValuesEqual(left, right) {
21507
+ try {
21508
+ return canonicalProjectRegistrationJson(left) === canonicalProjectRegistrationJson(right);
21509
+ } catch {
21510
+ return false;
21511
+ }
21512
+ }
21145
21513
  function receiptId(input) {
21146
21514
  return `tpr_${digestProjectRegistrationValue(input).slice(0, 40)}`;
21147
21515
  }
@@ -21179,9 +21547,37 @@ function normalizedCallDigest(request) {
21179
21547
  project_slug: request.project_slug,
21180
21548
  project_name: request.project_name,
21181
21549
  desired: request.desired,
21550
+ bind_existing: request.bind_existing === true,
21182
21551
  accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
21183
21552
  });
21184
21553
  }
21554
+ function legacyNormalizedCallDigestBeforeBindExisting(request) {
21555
+ return digestProjectRegistrationValue({
21556
+ authority_route: request.authority_route,
21557
+ package_version: request.package_version,
21558
+ authority_id: request.authority_id,
21559
+ tenant_id: request.tenant_id,
21560
+ corpus_id: request.corpus_id,
21561
+ operation_id: request.operation_id,
21562
+ step_id: request.step_id,
21563
+ resource_kind: request.resource_kind,
21564
+ direction: request.direction,
21565
+ target_selector: request.target_selector,
21566
+ idempotency_key: request.idempotency_key,
21567
+ request_digest: request.request_digest,
21568
+ precondition_digest: request.precondition_digest,
21569
+ project_id: request.project_id,
21570
+ project_slug: request.project_slug,
21571
+ project_name: request.project_name,
21572
+ desired: request.desired,
21573
+ accepted_receipt_id: request.accepted_receipt?.receipt_id ?? null
21574
+ });
21575
+ }
21576
+ function acceptedCallMatches(request, accepted, callDigest = normalizedCallDigest(request)) {
21577
+ if (accepted.normalized_call_digest === callDigest)
21578
+ return true;
21579
+ return request.bind_existing !== true && accepted.normalized_call_digest === legacyNormalizedCallDigestBeforeBindExisting(request);
21580
+ }
21185
21581
  function assertCommonRequest(request, capability) {
21186
21582
  assertBounds(request);
21187
21583
  assertResourceKind(request.resource_kind);
@@ -21223,6 +21619,9 @@ function assertCommonRequest(request, capability) {
21223
21619
  if (!request.desired || typeof request.desired !== "object" || Array.isArray(request.desired)) {
21224
21620
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "desired must be a JSON object");
21225
21621
  }
21622
+ if (request.bind_existing !== undefined && typeof request.bind_existing !== "boolean") {
21623
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "bind_existing must be boolean when supplied");
21624
+ }
21226
21625
  const expectedKey = deriveTodosProjectRegistrationIdempotencyKey({
21227
21626
  operation_id: request.operation_id,
21228
21627
  step_id: request.step_id,
@@ -21244,7 +21643,7 @@ function assertForwardRequest(request, capability) {
21244
21643
  const expectedRequestDigest = digestProjectRegistrationValue(request.desired);
21245
21644
  const expectedPreconditionDigest = digestProjectRegistrationValue({
21246
21645
  target_selector: request.target_selector,
21247
- expected: "absent"
21646
+ expected: request.bind_existing === true ? "absent_or_matching_existing" : "absent"
21248
21647
  });
21249
21648
  if (request.request_digest !== expectedRequestDigest || request.precondition_digest !== expectedPreconditionDigest) {
21250
21649
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_DIGEST_MISMATCH", "request_digest or precondition_digest does not match normalized forward semantics", {
@@ -21323,7 +21722,7 @@ function receiptBase(request, callDigest, capability) {
21323
21722
  normalized_call_digest: callDigest
21324
21723
  };
21325
21724
  }
21326
- function makeAcceptedReceipt(request, callDigest, capability, record, createdAt) {
21725
+ function makeAcceptedReceipt(request, callDigest, capability, record, createdAt, createdByOperation = true) {
21327
21726
  return makeReceipt({
21328
21727
  ...receiptBase(request, callDigest, capability),
21329
21728
  outcome: "accepted",
@@ -21333,7 +21732,7 @@ function makeAcceptedReceipt(request, callDigest, capability, record, createdAt)
21333
21732
  result_digest: record.digest,
21334
21733
  duplicate_of_receipt_id: null,
21335
21734
  accepted_receipt_id: request.direction === "inverse" ? request.accepted_receipt.receipt_id : null,
21336
- created_by_operation: true
21735
+ created_by_operation: createdByOperation
21337
21736
  }, createdAt);
21338
21737
  }
21339
21738
  function makeDuplicateReceipt(request, callDigest, capability, accepted, createdAt) {
@@ -21395,6 +21794,53 @@ function bindingFor(request, callDigest, timestamp2, capability) {
21395
21794
  updated_at: timestamp2
21396
21795
  };
21397
21796
  }
21797
+ function encodeProjectResourceCursor(input) {
21798
+ return Buffer.from(JSON.stringify({
21799
+ version: PROJECT_RESOURCE_CURSOR_VERSION,
21800
+ ...input
21801
+ }), "utf8").toString("base64url");
21802
+ }
21803
+ function decodeProjectResourceCursor(cursor, expected) {
21804
+ if (!cursor)
21805
+ return null;
21806
+ let parsed;
21807
+ try {
21808
+ parsed = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
21809
+ } catch {
21810
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "cursor is not a valid project-resource cursor");
21811
+ }
21812
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
21813
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "cursor is not a valid project-resource cursor");
21814
+ }
21815
+ const value = parsed;
21816
+ if (value["version"] !== PROJECT_RESOURCE_CURSOR_VERSION || value["source_project_id"] !== expected.source_project_id || value["include_anchors"] !== expected.include_anchors || !Number.isSafeInteger(value["kind_rank"]) || Number(value["kind_rank"]) < 0 || Number(value["kind_rank"]) > 3 || typeof value["target_id"] !== "string" || !UUID_PATTERN.test(value["target_id"]) || typeof value["collection_revision"] !== "string" || value["collection_revision"].length < 16 || value["collection_revision"].length > 128) {
21817
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "cursor does not match this project-resource query");
21818
+ }
21819
+ return {
21820
+ kind_rank: Number(value["kind_rank"]),
21821
+ target_id: value["target_id"],
21822
+ collection_revision: value["collection_revision"]
21823
+ };
21824
+ }
21825
+ function projectResourceFromCandidate(sourceProjectId, candidate) {
21826
+ const scope = candidate.kind === "project" || candidate.kind === "task_list" ? "collection" : "resource";
21827
+ return {
21828
+ source_project_id: sourceProjectId,
21829
+ kind: candidate.kind,
21830
+ scope,
21831
+ target_id: candidate.target_id,
21832
+ parent_id: candidate.parent_id,
21833
+ revision: candidate.revision,
21834
+ digest: digestProjectRegistrationValue({
21835
+ source_project_id: sourceProjectId,
21836
+ kind: candidate.kind,
21837
+ scope,
21838
+ target_id: candidate.target_id,
21839
+ parent_id: candidate.parent_id,
21840
+ revision: candidate.revision
21841
+ })
21842
+ };
21843
+ }
21398
21844
 
21399
21845
  class PackageOwnedTodosProjectRegistrationAuthority {
21400
21846
  backend;
@@ -21416,6 +21862,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
21416
21862
  immutable_receipts: true,
21417
21863
  exact_terminal_lookup: true,
21418
21864
  exact_readback: true,
21865
+ bind_existing_adoption: true,
21866
+ prior_registration_adoption_validation: true,
21867
+ project_resource_enumeration: true,
21868
+ project_resource_page_limit: PROJECT_RESOURCE_PAGE_LIMIT,
21419
21869
  conditional_inverse: true,
21420
21870
  ambiguous_outcome_reconciliation: true
21421
21871
  };
@@ -21460,6 +21910,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
21460
21910
  async existingForwardResolution(transaction, request, callDigest) {
21461
21911
  const exact = await transaction.getReceiptForLookup({
21462
21912
  ...authorityScope(this.capabilityValue),
21913
+ route: this.capabilityValue.route,
21914
+ package_version: this.capabilityValue.package_version,
21463
21915
  operation_id: request.operation_id,
21464
21916
  step_id: request.step_id,
21465
21917
  resource_kind: request.resource_kind,
@@ -21474,7 +21926,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
21474
21926
  if (!accepted2) {
21475
21927
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "duplicate receipt points to a missing accepted receipt");
21476
21928
  }
21477
- if (accepted2.normalized_call_digest !== callDigest) {
21929
+ if (!acceptedCallMatches(request, accepted2, callDigest)) {
21478
21930
  return this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted2.target_id });
21479
21931
  }
21480
21932
  return this.duplicateFor(transaction, request, callDigest, accepted2);
@@ -21488,7 +21940,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
21488
21940
  });
21489
21941
  if (!accepted)
21490
21942
  return null;
21491
- if (accepted.normalized_call_digest === callDigest) {
21943
+ if (acceptedCallMatches(request, accepted, callDigest)) {
21492
21944
  return this.duplicateFor(transaction, request, callDigest, accepted);
21493
21945
  }
21494
21946
  return this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted.target_id });
@@ -21499,6 +21951,12 @@ class PackageOwnedTodosProjectRegistrationAuthority {
21499
21951
  const slug2 = taskListSlug(request.project_slug);
21500
21952
  const conflict2 = await transaction.findProjectConflict(path, slug2);
21501
21953
  if (conflict2) {
21954
+ if (request.bind_existing === true && conflict2.path === path && conflict2.task_list_id === slug2) {
21955
+ return {
21956
+ record: boundExistingProjectRecord(conflict2),
21957
+ created_by_operation: false
21958
+ };
21959
+ }
21502
21960
  return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict2.id });
21503
21961
  }
21504
21962
  await this.fault("before_object_write", request);
@@ -21510,7 +21968,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
21510
21968
  task_prefix: deterministicTaskPrefix(request.project_slug)
21511
21969
  });
21512
21970
  await this.fault("after_object_write", request);
21513
- return projectRecord(project);
21971
+ return {
21972
+ record: projectRecord(project),
21973
+ created_by_operation: true
21974
+ };
21514
21975
  }
21515
21976
  const todosProjectId = String(request.desired["todos_project_id"]);
21516
21977
  const sourceBinding = await transaction.getBinding(authorityScope(this.capabilityValue), "project", request.project_id);
@@ -21524,6 +21985,12 @@ class PackageOwnedTodosProjectRegistrationAuthority {
21524
21985
  const slug = taskListSlug(request.project_slug);
21525
21986
  const conflict = await transaction.findTaskListConflict(todosProjectId, slug);
21526
21987
  if (conflict) {
21988
+ if (request.bind_existing === true && conflict.project_id === todosProjectId && conflict.slug === slug) {
21989
+ return {
21990
+ record: boundExistingTaskListRecord(conflict),
21991
+ created_by_operation: false
21992
+ };
21993
+ }
21527
21994
  return this.terminalFor(transaction, request, normalizedCallDigest(request), "target_already_exists", { targetId: conflict.id });
21528
21995
  }
21529
21996
  await this.fault("before_object_write", request);
@@ -21540,7 +22007,10 @@ class PackageOwnedTodosProjectRegistrationAuthority {
21540
22007
  if (taskList.project_id !== todosProjectId) {
21541
22008
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CONFLICT", "task-list create did not preserve the exact full Todos project id");
21542
22009
  }
21543
- return taskListRecord(taskList);
22010
+ return {
22011
+ record: taskListRecord(taskList),
22012
+ created_by_operation: true
22013
+ };
21544
22014
  }
21545
22015
  async create(request) {
21546
22016
  const startedAt = Date.now();
@@ -21562,9 +22032,9 @@ class PackageOwnedTodosProjectRegistrationAuthority {
21562
22032
  const claimed = await transaction.claimBinding(bindingFor(request, callDigest, timestamp2, this.capabilityValue));
21563
22033
  if (!claimed) {
21564
22034
  const binding = await transaction.getBinding(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector);
21565
- if (binding?.state === "accepted" && binding.normalized_call_digest === callDigest && binding.accepted_receipt_id) {
22035
+ if (binding?.state === "accepted" && binding.accepted_receipt_id) {
21566
22036
  const accepted2 = await transaction.getReceiptById(binding.accepted_receipt_id);
21567
- if (accepted2) {
22037
+ if (accepted2 && binding.normalized_call_digest === accepted2.normalized_call_digest && acceptedCallMatches(request, accepted2, callDigest)) {
21568
22038
  return this.duplicateFor(transaction, request, callDigest, accepted2);
21569
22039
  }
21570
22040
  }
@@ -21575,15 +22045,15 @@ class PackageOwnedTodosProjectRegistrationAuthority {
21575
22045
  await transaction.setBindingTerminal(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector, this.now());
21576
22046
  return recordOrTerminal;
21577
22047
  }
21578
- const accepted = makeAcceptedReceipt(request, callDigest, this.capabilityValue, recordOrTerminal, this.now());
22048
+ const accepted = makeAcceptedReceipt(request, callDigest, this.capabilityValue, recordOrTerminal.record, this.now(), recordOrTerminal.created_by_operation);
21579
22049
  await this.fault("before_receipt_write", request);
21580
22050
  const stored = await insertDeterministicReceipt(transaction, accepted);
21581
22051
  await this.fault("after_receipt_write", request);
21582
22052
  await transaction.setBindingAccepted(authorityScope(this.capabilityValue), request.resource_kind, request.target_selector, {
21583
- target_id: recordOrTerminal.target_id,
22053
+ target_id: recordOrTerminal.record.target_id,
21584
22054
  accepted_receipt_id: stored.receipt_id,
21585
- result_revision: recordOrTerminal.revision,
21586
- result_digest: recordOrTerminal.digest,
22055
+ result_revision: recordOrTerminal.record.revision,
22056
+ result_digest: recordOrTerminal.record.digest,
21587
22057
  updated_at: this.now()
21588
22058
  });
21589
22059
  return stored;
@@ -21612,6 +22082,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
21612
22082
  });
21613
22083
  const exact = await transaction.getReceiptForLookup({
21614
22084
  ...authorityScope(this.capabilityValue),
22085
+ route: this.capabilityValue.route,
22086
+ package_version: this.capabilityValue.package_version,
21615
22087
  operation_id: request.operation_id,
21616
22088
  step_id: request.step_id,
21617
22089
  resource_kind: request.resource_kind,
@@ -21629,7 +22101,7 @@ class PackageOwnedTodosProjectRegistrationAuthority {
21629
22101
  direction: request.direction
21630
22102
  });
21631
22103
  if (accepted) {
21632
- return accepted.normalized_call_digest === callDigest ? this.duplicateFor(transaction, request, callDigest, accepted) : this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted.target_id });
22104
+ return acceptedCallMatches(request, accepted, callDigest) ? this.duplicateFor(transaction, request, callDigest, accepted) : this.terminalFor(transaction, request, callDigest, "operation_step_semantics_changed", { targetId: accepted.target_id });
21633
22105
  }
21634
22106
  const timestamp2 = this.now();
21635
22107
  const claimed = await transaction.claimBinding(bindingFor(request, callDigest, timestamp2, this.capabilityValue));
@@ -21662,9 +22134,18 @@ class PackageOwnedTodosProjectRegistrationAuthority {
21662
22134
  if (request.max_items !== 1) {
21663
22135
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_BOUNDS", "max_items must be exactly 1 for terminal receipt lookup");
21664
22136
  }
21665
- if (request.authority !== "todos" || request.authority_route !== this.capabilityValue.route || request.package_version !== this.capabilityValue.package_version || request.authority_id !== this.capabilityValue.authority_id || request.tenant_id !== this.capabilityValue.tenant_id || request.corpus_id !== this.capabilityValue.corpus_id) {
22137
+ if (request.authority !== "todos" || request.authority_id !== this.capabilityValue.authority_id || request.tenant_id !== this.capabilityValue.tenant_id || request.corpus_id !== this.capabilityValue.corpus_id) {
21666
22138
  throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_CAPABILITY_MISMATCH", "receipt lookup does not match this authority capability identity");
21667
22139
  }
22140
+ requireString(request.authority_route, "authority_route", {
22141
+ min: 3,
22142
+ max: 128,
22143
+ pattern: AUTHORITY_ROUTE_PATTERN
22144
+ });
22145
+ requireString(request.package_version, "package_version", {
22146
+ max: 128,
22147
+ pattern: PACKAGE_VERSION_PATTERN
22148
+ });
21668
22149
  requireString(request.operation_id, "operation_id", {
21669
22150
  min: 8,
21670
22151
  max: 128,
@@ -21686,6 +22167,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
21686
22167
  }
21687
22168
  const receipt = await this.backend.getReceiptForLookup({
21688
22169
  ...authorityScope(this.capabilityValue),
22170
+ route: request.authority_route,
22171
+ package_version: request.package_version,
21689
22172
  operation_id: request.operation_id,
21690
22173
  step_id: request.step_id,
21691
22174
  resource_kind: request.resource_kind,
@@ -21698,6 +22181,164 @@ class PackageOwnedTodosProjectRegistrationAuthority {
21698
22181
  }
21699
22182
  return withResponseControl({ receipt: publicReceipt(receipt) }, request, startedAt);
21700
22183
  }
22184
+ async listProjectResources(request) {
22185
+ const sourceProjectId = requireString(request.source_project_id, "source_project_id", { min: 16, max: 128, pattern: WORKSPACE_ID_PATTERN });
22186
+ if (!Number.isSafeInteger(request.limit) || request.limit <= 0 || request.limit > PROJECT_RESOURCE_PAGE_LIMIT) {
22187
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_BOUNDS", `limit must be an integer from 1 to ${PROJECT_RESOURCE_PAGE_LIMIT}`);
22188
+ }
22189
+ if (request.include_anchors !== undefined && typeof request.include_anchors !== "boolean") {
22190
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_INVALID_INPUT", "include_anchors must be boolean when supplied");
22191
+ }
22192
+ const includeAnchors = request.include_anchors === true;
22193
+ const projectBinding = await this.backend.getBinding(authorityScope(this.capabilityValue), "project", sourceProjectId);
22194
+ if (!projectBinding || projectBinding.state !== "accepted" || !projectBinding.target_id) {
22195
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_RECORD_NOT_FOUND", "no accepted Todos project binding exists for this exact Projects workspace id", { source_project_id: sourceProjectId });
22196
+ }
22197
+ const taskListBinding = await this.backend.getBinding(authorityScope(this.capabilityValue), "task_list", `${projectBinding.target_id}:default`);
22198
+ if (!taskListBinding || taskListBinding.state !== "accepted" || !taskListBinding.target_id) {
22199
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_RECORD_NOT_FOUND", "no accepted canonical task-list binding exists for this exact Todos project id", {
22200
+ source_project_id: sourceProjectId,
22201
+ todos_project_id: projectBinding.target_id
22202
+ });
22203
+ }
22204
+ const cursor = decodeProjectResourceCursor(request.cursor, {
22205
+ source_project_id: sourceProjectId,
22206
+ include_anchors: includeAnchors
22207
+ });
22208
+ const collectionInput = {
22209
+ todos_project_id: projectBinding.target_id,
22210
+ task_list_id: taskListBinding.target_id,
22211
+ include_anchors: includeAnchors
22212
+ };
22213
+ const collectionRevision = await this.backend.getProjectResourceCollectionRevision(collectionInput);
22214
+ if (cursor && cursor.collection_revision !== collectionRevision) {
22215
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_COLLECTION_CHANGED", "project-resource collection changed during pagination; restart from the first page", {
22216
+ source_project_id: sourceProjectId,
22217
+ expected_collection_revision: cursor.collection_revision,
22218
+ current_collection_revision: collectionRevision
22219
+ });
22220
+ }
22221
+ const candidates = await this.backend.listProjectResourceCandidates({
22222
+ ...collectionInput,
22223
+ after: cursor ? { kind_rank: cursor.kind_rank, target_id: cursor.target_id } : null,
22224
+ limit: request.limit + 1
22225
+ });
22226
+ const verifiedCollectionRevision = await this.backend.getProjectResourceCollectionRevision(collectionInput);
22227
+ if (verifiedCollectionRevision !== collectionRevision) {
22228
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_COLLECTION_CHANGED", "project-resource collection changed while producing a page; restart from the first page", {
22229
+ source_project_id: sourceProjectId,
22230
+ expected_collection_revision: collectionRevision,
22231
+ current_collection_revision: verifiedCollectionRevision
22232
+ });
22233
+ }
22234
+ const hasMore = candidates.length > request.limit;
22235
+ const pageCandidates = candidates.slice(0, request.limit);
22236
+ const resources = pageCandidates.map((candidate) => projectResourceFromCandidate(sourceProjectId, candidate));
22237
+ const last = pageCandidates.at(-1);
22238
+ return {
22239
+ authority: "todos",
22240
+ route: this.capabilityValue.route,
22241
+ package_version: this.capabilityValue.package_version,
22242
+ authority_id: this.capabilityValue.authority_id,
22243
+ tenant_id: this.capabilityValue.tenant_id,
22244
+ corpus_id: this.capabilityValue.corpus_id,
22245
+ source_project_id: sourceProjectId,
22246
+ todos_project_id: projectBinding.target_id,
22247
+ task_list_id: taskListBinding.target_id,
22248
+ include_anchors: includeAnchors,
22249
+ collection_revision: collectionRevision,
22250
+ limit: request.limit,
22251
+ count: resources.length,
22252
+ resources,
22253
+ has_more: hasMore,
22254
+ next_cursor: hasMore && last ? encodeProjectResourceCursor({
22255
+ source_project_id: sourceProjectId,
22256
+ include_anchors: includeAnchors,
22257
+ collection_revision: collectionRevision,
22258
+ kind_rank: last.kind_rank,
22259
+ target_id: last.target_id
22260
+ }) : null,
22261
+ complete: !hasMore,
22262
+ truncated: false
22263
+ };
22264
+ }
22265
+ async validatePriorRegistrationAdoption(sourceRequest, sourceReceipt, currentRecord) {
22266
+ const startedAt = Date.now();
22267
+ if (!sourceRequest || typeof sourceRequest !== "object" || Array.isArray(sourceRequest) || !sourceReceipt || typeof sourceReceipt !== "object" || Array.isArray(sourceReceipt) || !currentRecord || typeof currentRecord !== "object" || Array.isArray(currentRecord)) {
22268
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source request, source receipt, and current record must be present objects");
22269
+ }
22270
+ requireString(sourceRequest.package_version, "package_version", {
22271
+ max: 128,
22272
+ pattern: PACKAGE_VERSION_PATTERN
22273
+ });
22274
+ assertForwardRequest(sourceRequest, {
22275
+ ...this.capabilityValue,
22276
+ package_version: sourceRequest.package_version
22277
+ });
22278
+ const validation = await this.backend.transaction(async (transaction) => {
22279
+ const storedSource = await transaction.getReceiptById(sourceReceipt.receipt_id);
22280
+ if (!storedSource || !canonicalValuesEqual(publicReceipt(storedSource), sourceReceipt) || storedSource.outcome !== "accepted" && storedSource.outcome !== "duplicate_of_accepted") {
22281
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source receipt is not an exact immutable accepted or duplicate receipt", { receipt_id: sourceReceipt.receipt_id });
22282
+ }
22283
+ const accepted = storedSource.outcome === "accepted" ? storedSource : storedSource.duplicate_of_receipt_id ? await transaction.getReceiptById(storedSource.duplicate_of_receipt_id) : null;
22284
+ if (!accepted || accepted.outcome !== "accepted" || !accepted.target_id || !accepted.result_revision || !accepted.result_digest) {
22285
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "source receipt does not resolve to one complete accepted receipt", { receipt_id: sourceReceipt.receipt_id });
22286
+ }
22287
+ const receiptLineageMatches = (receipt) => receipt.authority === "todos" && receipt.route === sourceRequest.authority_route && receipt.package_version === sourceRequest.package_version && receipt.authority_id === sourceRequest.authority_id && receipt.tenant_id === sourceRequest.tenant_id && receipt.corpus_id === sourceRequest.corpus_id && receipt.operation_id === sourceRequest.operation_id && receipt.step_id === sourceRequest.step_id && receipt.resource_kind === sourceRequest.resource_kind && receipt.direction === "forward" && receipt.target_selector === sourceRequest.target_selector && receipt.idempotency_key === sourceRequest.idempotency_key && receipt.request_digest === sourceRequest.request_digest && receipt.precondition_digest === sourceRequest.precondition_digest && acceptedCallMatches(sourceRequest, receipt);
22288
+ if (!receiptLineageMatches(storedSource) || !receiptLineageMatches(accepted) || storedSource.outcome === "duplicate_of_accepted" && (storedSource.duplicate_of_receipt_id !== accepted.receipt_id || storedSource.target_id !== accepted.target_id || storedSource.result_revision !== accepted.result_revision || storedSource.result_digest !== accepted.result_digest)) {
22289
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "receipt authority, operation, request, precondition, or normalized-call lineage changed");
22290
+ }
22291
+ const binding = await transaction.getBinding(authorityScope(this.capabilityValue), sourceRequest.resource_kind, sourceRequest.target_selector);
22292
+ if (!binding || binding.state !== "accepted" || binding.operation_id !== sourceRequest.operation_id || binding.step_id !== sourceRequest.step_id || binding.direction !== "forward" || binding.idempotency_key !== sourceRequest.idempotency_key || binding.request_digest !== sourceRequest.request_digest || binding.precondition_digest !== sourceRequest.precondition_digest || binding.normalized_call_digest !== accepted.normalized_call_digest || binding.target_id !== accepted.target_id || binding.accepted_receipt_id !== accepted.receipt_id || binding.result_revision !== accepted.result_revision || binding.result_digest !== accepted.result_digest) {
22293
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "accepted binding does not match the exact accepted receipt and source lineage");
22294
+ }
22295
+ const current = sourceRequest.resource_kind === "project" ? await transaction.getProject(accepted.target_id) : await transaction.getTaskList(accepted.target_id);
22296
+ if (!current || !canonicalValuesEqual(current, currentRecord) || current.id !== accepted.target_id || current.created_at !== accepted.result_revision) {
22297
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "current record does not match the accepted target incarnation", { target_id: accepted.target_id });
22298
+ }
22299
+ let stableMatch = false;
22300
+ if (sourceRequest.resource_kind === "task_list") {
22301
+ stableMatch = taskListRegistrationDigest({
22302
+ ...current,
22303
+ updated_at: accepted.result_revision
22304
+ }) === accepted.result_digest;
22305
+ } else {
22306
+ const project = current;
22307
+ if (!Number.isSafeInteger(project.task_counter) || project.task_counter < 0) {
22308
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "current project task counter is not a valid monotonic registration field");
22309
+ }
22310
+ for (let priorTaskCounter = 0;priorTaskCounter <= project.task_counter; priorTaskCounter += 1) {
22311
+ if (Date.now() - startedAt > sourceRequest.time_budget_ms) {
22312
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_TIME_BUDGET_EXCEEDED", "prior registration adoption validation exceeded its time budget");
22313
+ }
22314
+ if (projectRegistrationDigest({
22315
+ ...project,
22316
+ task_counter: priorTaskCounter,
22317
+ updated_at: accepted.result_revision
22318
+ }) === accepted.result_digest) {
22319
+ stableMatch = true;
22320
+ break;
22321
+ }
22322
+ }
22323
+ }
22324
+ if (!stableMatch) {
22325
+ throw new TodosProjectRegistrationError("TODOS_PROJECT_REGISTRATION_ADOPTION_REJECTED", "stable project-registration fields changed after the accepted receipt", { target_id: accepted.target_id });
22326
+ }
22327
+ return {
22328
+ valid: true,
22329
+ resource_kind: sourceRequest.resource_kind,
22330
+ target_id: accepted.target_id,
22331
+ source_receipt_id: storedSource.receipt_id,
22332
+ accepted_receipt_id: accepted.receipt_id,
22333
+ source_outcome: storedSource.outcome,
22334
+ created_at: current.created_at,
22335
+ current_revision: current.updated_at,
22336
+ accepted_result_digest: accepted.result_digest
22337
+ };
22338
+ });
22339
+ assertWithinBounds(validation, sourceRequest, startedAt);
22340
+ return validation;
22341
+ }
21701
22342
  async storedAcceptedReceipt(request, supplied) {
21702
22343
  const stored = await this.backend.getReceiptById(supplied.receipt_id);
21703
22344
  if (!stored || stored.outcome !== "accepted" || !stored.created_by_operation || canonicalProjectRegistrationJson(publicReceipt(stored)) !== canonicalProjectRegistrationJson(supplied)) {
@@ -21724,6 +22365,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
21724
22365
  });
21725
22366
  const exact = await transaction.getReceiptForLookup({
21726
22367
  ...authorityScope(this.capabilityValue),
22368
+ route: this.capabilityValue.route,
22369
+ package_version: this.capabilityValue.package_version,
21727
22370
  operation_id: request.operation_id,
21728
22371
  step_id: request.step_id,
21729
22372
  resource_kind: request.resource_kind,
@@ -21818,6 +22461,8 @@ class PackageOwnedTodosProjectRegistrationAuthority {
21818
22461
  await this.storedAcceptedReceipt(request, accepted);
21819
22462
  const receipt = await this.backend.getReceiptForLookup({
21820
22463
  ...authorityScope(this.capabilityValue),
22464
+ route: this.capabilityValue.route,
22465
+ package_version: this.capabilityValue.package_version,
21821
22466
  operation_id: request.operation_id,
21822
22467
  step_id: request.step_id,
21823
22468
  resource_kind: request.resource_kind,
@@ -21862,7 +22507,7 @@ function createPostgresTodosProjectRegistrationAuthority(client, options = {}) {
21862
22507
  cursorTableName
21863
22508
  }), authorityOptions);
21864
22509
  }
21865
- var UUID_PATTERN, WORKSPACE_ID_PATTERN, OPERATION_PATTERN, STEP_PATTERN, SHA256_PATTERN, IDEMPOTENCY_PATTERN, WriteBoundaryError;
22510
+ var UUID_PATTERN, WORKSPACE_ID_PATTERN, OPERATION_PATTERN, STEP_PATTERN, AUTHORITY_ROUTE_PATTERN, PACKAGE_VERSION_PATTERN, SHA256_PATTERN, IDEMPOTENCY_PATTERN, PROJECT_RESOURCE_PAGE_LIMIT = 500, PROJECT_RESOURCE_CURSOR_VERSION = 1, WriteBoundaryError;
21866
22511
  var init_authority = __esm(() => {
21867
22512
  init_package_version();
21868
22513
  init_postgres2();
@@ -21872,6 +22517,8 @@ var init_authority = __esm(() => {
21872
22517
  WORKSPACE_ID_PATTERN = /^wks_[A-Za-z0-9][A-Za-z0-9_-]{11,}$/;
21873
22518
  OPERATION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$/;
21874
22519
  STEP_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/;
22520
+ AUTHORITY_ROUTE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/;
22521
+ PACKAGE_VERSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,127}$/;
21875
22522
  SHA256_PATTERN = /^[0-9a-f]{64}$/;
21876
22523
  IDEMPOTENCY_PATTERN = /^prk_[0-9a-f]{48}$/;
21877
22524
  WriteBoundaryError = class WriteBoundaryError extends Error {
@@ -21885,6 +22532,11 @@ var init_authority = __esm(() => {
21885
22532
  };
21886
22533
  });
21887
22534
 
22535
+ // src/project-registration/adoption-validation.ts
22536
+ var init_adoption_validation = __esm(() => {
22537
+ init_types3();
22538
+ });
22539
+
21888
22540
  // src/project-registration/http.ts
21889
22541
  function json(body, status = 200) {
21890
22542
  return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS });
@@ -21930,6 +22582,20 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
21930
22582
  if ((action === "" || action === "capability") && method === "GET") {
21931
22583
  return json({ capability: await authority.capability() });
21932
22584
  }
22585
+ if (action === "resources" && method === "GET") {
22586
+ const sourceProjectId = url.searchParams.get("source_project_id");
22587
+ const limit = Number(url.searchParams.get("limit") ?? "100");
22588
+ const includeAnchorsRaw = url.searchParams.get("include_anchors");
22589
+ const includeAnchors = includeAnchorsRaw === null ? false : includeAnchorsRaw === "true" ? true : includeAnchorsRaw === "false" ? false : includeAnchorsRaw;
22590
+ return json({
22591
+ page: await authority.listProjectResources({
22592
+ source_project_id: sourceProjectId,
22593
+ limit,
22594
+ include_anchors: includeAnchors,
22595
+ cursor: url.searchParams.get("cursor") ?? undefined
22596
+ })
22597
+ });
22598
+ }
21933
22599
  if (method !== "POST")
21934
22600
  return json({ error: "method not allowed" }, 405);
21935
22601
  const body = await readJson(req);
@@ -21952,6 +22618,12 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
21952
22618
  record: await authority.readExact(body)
21953
22619
  });
21954
22620
  }
22621
+ if (action === "validate-prior-adoption") {
22622
+ const input = body;
22623
+ return json({
22624
+ validation: await authority.validatePriorRegistrationAdoption(input.source_request, input.source_receipt, input.current_record)
22625
+ });
22626
+ }
21955
22627
  if (action === "compensate") {
21956
22628
  return json({
21957
22629
  receipt: await authority.compensate(body)
@@ -21984,6 +22656,7 @@ async function handleTodosProjectRegistrationHttpRequest(req, url, authority, ba
21984
22656
  var JSON_HEADERS;
21985
22657
  var init_http = __esm(() => {
21986
22658
  init_types3();
22659
+ init_adoption_validation();
21987
22660
  JSON_HEADERS = { "Content-Type": "application/json" };
21988
22661
  });
21989
22662
 
@@ -21991,6 +22664,7 @@ var init_http = __esm(() => {
21991
22664
  var init_project_registration = __esm(() => {
21992
22665
  init_authority();
21993
22666
  init_http();
22667
+ init_adoption_validation();
21994
22668
  init_postgres2();
21995
22669
  init_sqlite();
21996
22670
  init_types3();
@@ -25963,7 +26637,7 @@ var init_zod = __esm(() => {
25963
26637
  });
25964
26638
 
25965
26639
  // src/task-manifest/types.ts
25966
- var TODOS_TASK_MANIFEST_ROUTE = "todos.task-manifest.v1", TODOS_TASK_MANIFEST_SCHEMA_VERSION = 1, TodosTaskManifestError;
26640
+ var TODOS_TASK_MANIFEST_ROUTE = "todos.task-manifest.v1", TODOS_TASK_MANIFEST_CALLER_ROUTE = "accounts.task-manifest.v1", TODOS_TASK_MANIFEST_SCHEMA_VERSION = 1, TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE = "deterministic-v1", TodosTaskManifestError;
25967
26641
  var init_types5 = __esm(() => {
25968
26642
  TodosTaskManifestError = class TodosTaskManifestError extends Error {
25969
26643
  code;
@@ -26050,7 +26724,7 @@ function parseTodosTaskManifestBindingLookup(input) {
26050
26724
  }
26051
26725
  return parsed.data;
26052
26726
  }
26053
- var TODOS_TASK_MANIFEST_BOUNDS, key, identifier, uuid2, scalar, boundedScalarRecord = (limit, field) => exports_external.record(exports_external.string().max(200), scalar).superRefine((value, context) => {
26727
+ var TODOS_TASK_MANIFEST_BOUNDS, key, identifier, digest, idempotencyKey, uuid2, scalar, boundedScalarRecord = (limit, field) => exports_external.record(exports_external.string().max(200), scalar).superRefine((value, context) => {
26054
26728
  if (Object.keys(value).length > limit) {
26055
26729
  context.addIssue({ code: exports_external.ZodIssueCode.custom, message: `${field} exceeds ${limit} fields` });
26056
26730
  }
@@ -26072,6 +26746,8 @@ var init_schema2 = __esm(() => {
26072
26746
  };
26073
26747
  key = exports_external.string().min(1).max(96).regex(/^[a-z][a-z0-9_-]*$/);
26074
26748
  identifier = exports_external.string().min(1).max(200).regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/);
26749
+ digest = exports_external.string().length(64).regex(/^[0-9a-f]{64}$/);
26750
+ idempotencyKey = exports_external.string().length(52).regex(/^tmk_[0-9a-f]{48}$/);
26075
26751
  uuid2 = exports_external.string().uuid();
26076
26752
  scalar = exports_external.union([exports_external.string().max(4096), exports_external.number().finite(), exports_external.boolean(), exports_external.null()]);
26077
26753
  comment = exports_external.object({
@@ -26109,7 +26785,9 @@ var init_schema2 = __esm(() => {
26109
26785
  schema = exports_external.object({
26110
26786
  version: exports_external.literal(1),
26111
26787
  operation_id: identifier,
26112
- idempotency_key: identifier,
26788
+ step_id: identifier,
26789
+ idempotency_key: idempotencyKey,
26790
+ precondition_digest: digest,
26113
26791
  project_id: uuid2,
26114
26792
  task_list_id: uuid2.optional(),
26115
26793
  if_binding_version: exports_external.number().int().min(0).optional(),
@@ -26125,7 +26803,10 @@ var init_schema2 = __esm(() => {
26125
26803
  }).strict();
26126
26804
  compensationSchema = exports_external.object({
26127
26805
  receipt_id: uuid2,
26128
- idempotency_key: identifier,
26806
+ operation_id: identifier,
26807
+ step_id: identifier,
26808
+ idempotency_key: idempotencyKey,
26809
+ precondition_digest: digest,
26129
26810
  if_binding_version: exports_external.number().int().min(1).max(Number.MAX_SAFE_INTEGER)
26130
26811
  }).strict();
26131
26812
  bindingLookupSchema = exports_external.object({
@@ -26143,6 +26824,7 @@ function taskManifestPlanSlug(manifest, planId) {
26143
26824
  const base = normalizeSlug(manifest.plan.key) || normalizeSlug(manifest.plan.name) || "plan";
26144
26825
  return `${base}-${planId}`;
26145
26826
  }
26827
+ var TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE = "deterministic-v1";
26146
26828
  var init_plan_slug = () => {};
26147
26829
 
26148
26830
  // src/task-manifest/backend.ts
@@ -26156,11 +26838,13 @@ function validateTaskManifestBindingLookupRows(rows, tenantId, planId) {
26156
26838
  const row = rows[0];
26157
26839
  const bindingVersion = Number(row.binding_version);
26158
26840
  const state = row.state;
26159
- if (row.binding_tenant_id !== tenantId || row.receipt_tenant_id !== tenantId || row.binding_plan_id !== planId || row.receipt_plan_id !== planId || row.receipt_authority !== "todos" || row.receipt_route !== "todos.task-manifest.v1" || Number(row.receipt_schema_version) !== 1 || row.receipt_kind !== "apply" || row.binding_operation_id !== row.receipt_operation_id || typeof row.apply_receipt_id !== "string" || !UUID_PATTERN2.test(row.apply_receipt_id) || !Number.isSafeInteger(bindingVersion) || bindingVersion < 1 || state !== "applied" && state !== "compensated") {
26841
+ if (row.binding_tenant_id !== tenantId || row.receipt_tenant_id !== tenantId || row.binding_plan_id !== planId || row.receipt_plan_id !== planId || row.receipt_authority !== "todos" || row.receipt_route !== "todos.task-manifest.v1" || Number(row.receipt_schema_version) !== 1 || row.receipt_kind !== "apply" || row.binding_operation_id !== row.receipt_operation_id || row.binding_step_id !== row.receipt_step_id || typeof row.binding_operation_id !== "string" || typeof row.binding_step_id !== "string" || typeof row.apply_receipt_id !== "string" || !UUID_PATTERN2.test(row.apply_receipt_id) || !Number.isSafeInteger(bindingVersion) || bindingVersion < 1 || state !== "applied" && state !== "compensated") {
26160
26842
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_LOOKUP_CONFLICT", "Task-manifest binding and immutable apply receipt disagree", { plan_id: planId });
26161
26843
  }
26162
26844
  return {
26163
26845
  plan_id: planId,
26846
+ operation_id: row.binding_operation_id,
26847
+ step_id: row.binding_step_id,
26164
26848
  apply_receipt_id: row.apply_receipt_id,
26165
26849
  binding_version: bindingVersion,
26166
26850
  state
@@ -26213,9 +26897,15 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
26213
26897
  schema_version integer NOT NULL CHECK(schema_version = 1),
26214
26898
  kind text NOT NULL CHECK(kind IN ('apply', 'compensate')),
26215
26899
  operation_id text NOT NULL,
26900
+ step_id text NOT NULL,
26216
26901
  idempotency_key text NOT NULL,
26217
26902
  request_digest text NOT NULL,
26903
+ precondition_digest text NOT NULL,
26218
26904
  result_digest text NOT NULL,
26905
+ slug_provenance text,
26906
+ outcome text NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
26907
+ reason text,
26908
+ duplicate_of_receipt_id text,
26219
26909
  binding_version integer NOT NULL,
26220
26910
  apply_receipt_id text,
26221
26911
  manifest_json jsonb,
@@ -26227,12 +26917,28 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
26227
26917
  ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
26228
26918
  `ALTER TABLE todos_task_manifest_receipts
26229
26919
  ALTER COLUMN tenant_id DROP DEFAULT`,
26920
+ `ALTER TABLE todos_task_manifest_receipts
26921
+ ADD COLUMN IF NOT EXISTS slug_provenance text`,
26922
+ `ALTER TABLE todos_task_manifest_receipts
26923
+ ADD COLUMN IF NOT EXISTS step_id text NOT NULL DEFAULT 'legacy-apply'`,
26924
+ `ALTER TABLE todos_task_manifest_receipts
26925
+ ADD COLUMN IF NOT EXISTS precondition_digest text NOT NULL DEFAULT '${"0".repeat(64)}'`,
26926
+ `ALTER TABLE todos_task_manifest_receipts
26927
+ ADD COLUMN IF NOT EXISTS outcome text NOT NULL DEFAULT 'accepted'`,
26928
+ `ALTER TABLE todos_task_manifest_receipts
26929
+ ADD COLUMN IF NOT EXISTS reason text`,
26930
+ `ALTER TABLE todos_task_manifest_receipts
26931
+ ADD COLUMN IF NOT EXISTS duplicate_of_receipt_id text`,
26230
26932
  `CREATE TABLE IF NOT EXISTS todos_task_manifest_bindings (
26231
26933
  operation_id text PRIMARY KEY,
26232
26934
  tenant_id text NOT NULL,
26935
+ step_id text NOT NULL,
26233
26936
  idempotency_key text NOT NULL UNIQUE,
26234
26937
  request_digest text NOT NULL,
26938
+ precondition_digest text NOT NULL,
26235
26939
  result_digest text NOT NULL,
26940
+ slug_provenance text,
26941
+ outcome text NOT NULL CHECK(outcome IN ('accepted', 'terminal_nonacceptance')),
26236
26942
  apply_receipt_id text NOT NULL UNIQUE REFERENCES todos_task_manifest_receipts(receipt_id),
26237
26943
  manifest_json jsonb NOT NULL,
26238
26944
  result_json jsonb NOT NULL,
@@ -26246,6 +26952,14 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
26246
26952
  ADD COLUMN IF NOT EXISTS tenant_id text NOT NULL DEFAULT ${tenantDefault}`,
26247
26953
  `ALTER TABLE todos_task_manifest_bindings
26248
26954
  ALTER COLUMN tenant_id DROP DEFAULT`,
26955
+ `ALTER TABLE todos_task_manifest_bindings
26956
+ ADD COLUMN IF NOT EXISTS slug_provenance text`,
26957
+ `ALTER TABLE todos_task_manifest_bindings
26958
+ ADD COLUMN IF NOT EXISTS step_id text NOT NULL DEFAULT 'legacy-apply'`,
26959
+ `ALTER TABLE todos_task_manifest_bindings
26960
+ ADD COLUMN IF NOT EXISTS precondition_digest text NOT NULL DEFAULT '${"0".repeat(64)}'`,
26961
+ `ALTER TABLE todos_task_manifest_bindings
26962
+ ADD COLUMN IF NOT EXISTS outcome text NOT NULL DEFAULT 'accepted'`,
26249
26963
  `CREATE TABLE IF NOT EXISTS todos_task_manifest_outbox (
26250
26964
  id text PRIMARY KEY,
26251
26965
  apply_receipt_id text NOT NULL REFERENCES todos_task_manifest_receipts(receipt_id),
@@ -26257,10 +26971,37 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
26257
26971
  created_at timestamptz NOT NULL,
26258
26972
  delivered_at timestamptz
26259
26973
  )`,
26974
+ `CREATE TABLE IF NOT EXISTS todos_task_manifest_terminal_receipts (
26975
+ receipt_id text PRIMARY KEY,
26976
+ tenant_id text NOT NULL,
26977
+ authority text NOT NULL CHECK(authority = 'todos'),
26978
+ route text NOT NULL,
26979
+ schema_version integer NOT NULL CHECK(schema_version = 1),
26980
+ kind text NOT NULL CHECK(kind IN ('apply', 'compensate')),
26981
+ operation_id text NOT NULL,
26982
+ step_id text NOT NULL,
26983
+ idempotency_key text NOT NULL,
26984
+ request_digest text NOT NULL,
26985
+ precondition_digest text NOT NULL,
26986
+ result_digest text NOT NULL,
26987
+ outcome text NOT NULL CHECK(outcome = 'terminal_nonacceptance'),
26988
+ reason text NOT NULL,
26989
+ binding_version integer NOT NULL,
26990
+ apply_receipt_id text,
26991
+ manifest_json jsonb,
26992
+ result_json jsonb NOT NULL,
26993
+ created_at timestamptz NOT NULL
26994
+ )`,
26260
26995
  `CREATE INDEX IF NOT EXISTS todos_task_manifest_outbox_receipt_idx
26261
26996
  ON todos_task_manifest_outbox(apply_receipt_id, status)`,
26262
26997
  `CREATE INDEX IF NOT EXISTS todos_task_manifest_receipts_tenant_idx
26263
26998
  ON todos_task_manifest_receipts(tenant_id, receipt_id, kind)`,
26999
+ `DROP INDEX IF EXISTS todos_task_manifest_terminal_receipts_lookup_idx`,
27000
+ `DROP INDEX IF EXISTS todos_task_manifest_terminal_receipts_identity_idx`,
27001
+ `CREATE INDEX IF NOT EXISTS todos_task_manifest_terminal_receipts_lookup_idx
27002
+ ON todos_task_manifest_terminal_receipts(tenant_id, kind, operation_id, step_id)`,
27003
+ `CREATE UNIQUE INDEX IF NOT EXISTS todos_task_manifest_terminal_receipts_identity_idx
27004
+ ON todos_task_manifest_terminal_receipts(tenant_id, kind, operation_id, step_id)`,
26264
27005
  `CREATE INDEX IF NOT EXISTS todos_task_manifest_bindings_tenant_plan_idx
26265
27006
  ON todos_task_manifest_bindings(
26266
27007
  tenant_id,
@@ -26273,6 +27014,10 @@ function postgresTodosTaskManifestSchemaSql(tenantId = "default") {
26273
27014
  `DROP TRIGGER IF EXISTS todos_task_manifest_receipts_immutable ON todos_task_manifest_receipts`,
26274
27015
  `CREATE TRIGGER todos_task_manifest_receipts_immutable
26275
27016
  BEFORE UPDATE OR DELETE ON todos_task_manifest_receipts
27017
+ FOR EACH ROW EXECUTE FUNCTION todos_task_manifest_receipts_immutable()`,
27018
+ `DROP TRIGGER IF EXISTS todos_task_manifest_terminal_receipts_immutable ON todos_task_manifest_terminal_receipts`,
27019
+ `CREATE TRIGGER todos_task_manifest_terminal_receipts_immutable
27020
+ BEFORE UPDATE OR DELETE ON todos_task_manifest_terminal_receipts
26276
27021
  FOR EACH ROW EXECUTE FUNCTION todos_task_manifest_receipts_immutable()`
26277
27022
  ];
26278
27023
  }
@@ -26297,6 +27042,39 @@ function safeIdentifier2(value, field) {
26297
27042
  function parseJson(value) {
26298
27043
  return typeof value === "string" ? JSON.parse(value) : value;
26299
27044
  }
27045
+ function parseApplyResult(value, duplicate) {
27046
+ const parsed = parseJson(value);
27047
+ return {
27048
+ ...parsed,
27049
+ duplicate,
27050
+ receipt: {
27051
+ ...parsed.receipt,
27052
+ step_id: parsed.receipt.step_id ?? "legacy-apply",
27053
+ precondition_digest: parsed.receipt.precondition_digest ?? "0".repeat(64),
27054
+ outcome: parsed.receipt.outcome ?? "accepted",
27055
+ reason: parsed.receipt.reason ?? null,
27056
+ duplicate_of_receipt_id: parsed.receipt.duplicate_of_receipt_id ?? null
27057
+ }
27058
+ };
27059
+ }
27060
+ function validatePostgresPlanSlug(manifest, planId, slug, provenance) {
27061
+ if (provenance === TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE) {
27062
+ const expected = taskManifestPlanSlug(manifest, planId);
27063
+ if (slug !== expected) {
27064
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: plan slug changed since apply");
27065
+ }
27066
+ return expected;
27067
+ }
27068
+ if (provenance !== null && provenance !== undefined) {
27069
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: unknown plan slug provenance");
27070
+ }
27071
+ if (slug === null || slug === undefined)
27072
+ return null;
27073
+ if (slug !== null && slug !== undefined) {
27074
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: legacy PostgreSQL plan slug must be NULL");
27075
+ }
27076
+ return null;
27077
+ }
26300
27078
  function timestamp2(value) {
26301
27079
  return value instanceof Date ? value.toISOString() : new Date(String(value)).toISOString();
26302
27080
  }
@@ -26304,6 +27082,41 @@ function fault(faults, point) {
26304
27082
  if (faults.points.has(point))
26305
27083
  throw new Error(`Injected task-manifest fault at ${point}`);
26306
27084
  }
27085
+ function terminalApplyResult(input, reason) {
27086
+ const receipt = {
27087
+ receipt_id: input.terminal_receipt_id,
27088
+ authority: "todos",
27089
+ route: "todos.task-manifest.v1",
27090
+ schema_version: 1,
27091
+ kind: "apply",
27092
+ operation_id: input.manifest.operation_id,
27093
+ step_id: input.manifest.step_id,
27094
+ idempotency_key: input.manifest.idempotency_key,
27095
+ request_digest: input.request_digest,
27096
+ precondition_digest: input.manifest.precondition_digest,
27097
+ result_digest: canonicalDigest({
27098
+ outcome: "terminal_nonacceptance",
27099
+ reason,
27100
+ operation_id: input.manifest.operation_id,
27101
+ step_id: input.manifest.step_id,
27102
+ request_digest: input.request_digest
27103
+ }),
27104
+ outcome: "terminal_nonacceptance",
27105
+ reason,
27106
+ duplicate_of_receipt_id: null,
27107
+ binding_version: 0,
27108
+ apply_receipt_id: null,
27109
+ created_at: input.now
27110
+ };
27111
+ return {
27112
+ duplicate: false,
27113
+ receipt,
27114
+ graph: input.graph,
27115
+ readback: { plans: 0, tasks: 0, dependencies: 0, comments: 0, verifications: 0, complete: true },
27116
+ outbox_ids: [],
27117
+ result_digest: receipt.result_digest
27118
+ };
27119
+ }
26307
27120
  function receiptFromRow3(row) {
26308
27121
  return {
26309
27122
  receipt_id: String(row["receipt_id"]),
@@ -26312,9 +27125,14 @@ function receiptFromRow3(row) {
26312
27125
  schema_version: 1,
26313
27126
  kind: row["kind"],
26314
27127
  operation_id: String(row["operation_id"]),
27128
+ step_id: String(row["step_id"] ?? "legacy-apply"),
26315
27129
  idempotency_key: String(row["idempotency_key"]),
26316
27130
  request_digest: String(row["request_digest"]),
27131
+ precondition_digest: String(row["precondition_digest"] ?? "0".repeat(64)),
26317
27132
  result_digest: String(row["result_digest"]),
27133
+ outcome: row["outcome"] ?? "accepted",
27134
+ reason: row["reason"] == null ? null : row["reason"],
27135
+ duplicate_of_receipt_id: row["duplicate_of_receipt_id"] == null ? null : String(row["duplicate_of_receipt_id"]),
26318
27136
  binding_version: Number(row["binding_version"]),
26319
27137
  apply_receipt_id: row["apply_receipt_id"] == null ? null : String(row["apply_receipt_id"]),
26320
27138
  created_at: timestamp2(row["created_at"])
@@ -26432,46 +27250,89 @@ class PostgresTodosTaskManifestBackend {
26432
27250
  now3
26433
27251
  ]);
26434
27252
  }
27253
+ async persistTerminal(tx, input, reason) {
27254
+ const result = terminalApplyResult(input, reason);
27255
+ const resultJson = canonicalJson(result);
27256
+ await tx.query(`INSERT INTO todos_task_manifest_terminal_receipts (
27257
+ receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id,
27258
+ idempotency_key, request_digest, precondition_digest, result_digest, outcome, reason,
27259
+ binding_version, apply_receipt_id, manifest_json, result_json, created_at
27260
+ ) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'apply', $3, $4, $5, $6, $7, $8,
27261
+ 'terminal_nonacceptance', $9, 0, NULL, $10::jsonb, $11::jsonb, $12)
27262
+ ON CONFLICT (tenant_id, kind, operation_id, step_id) DO NOTHING`, [
27263
+ result.receipt.receipt_id,
27264
+ this.tenantId,
27265
+ input.manifest.operation_id,
27266
+ input.manifest.step_id,
27267
+ input.manifest.idempotency_key,
27268
+ input.request_digest,
27269
+ input.manifest.precondition_digest,
27270
+ result.receipt.result_digest,
27271
+ reason,
27272
+ canonicalJson(input.manifest),
27273
+ resultJson,
27274
+ input.now
27275
+ ]);
27276
+ const stored = await tx.query(`SELECT receipt_id, result_json
27277
+ FROM todos_task_manifest_terminal_receipts
27278
+ WHERE tenant_id = $1 AND kind = 'apply'
27279
+ AND (receipt_id = $2 OR (operation_id = $3 AND step_id = $4))
27280
+ ORDER BY created_at ASC, receipt_id ASC
27281
+ LIMIT 1`, [this.tenantId, result.receipt.receipt_id, input.manifest.operation_id, input.manifest.step_id]);
27282
+ return stored.rows[0] ? parseApplyResult(stored.rows[0]["result_json"], stored.rows[0]["receipt_id"] !== result.receipt.receipt_id) : result;
27283
+ }
26435
27284
  async apply(input, faults) {
26436
27285
  await this.ensureSchema();
26437
27286
  return this.client.transaction(async (tx) => {
26438
27287
  const { manifest } = input;
26439
27288
  await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1F${manifest.operation_id}`]);
26440
27289
  await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1Fidempotency\x1F${manifest.idempotency_key}`]);
27290
+ const terminal = await tx.query(`SELECT result_json FROM todos_task_manifest_terminal_receipts
27291
+ WHERE tenant_id = $1
27292
+ AND kind = 'apply'
27293
+ AND (receipt_id = $2 OR (operation_id = $3 AND step_id = $4))
27294
+ ORDER BY created_at ASC, receipt_id ASC
27295
+ LIMIT 1`, [this.tenantId, input.terminal_receipt_id, manifest.operation_id, manifest.step_id]);
27296
+ if (terminal.rows[0]) {
27297
+ return parseApplyResult(terminal.rows[0]["result_json"], true);
27298
+ }
26441
27299
  const existing = await tx.query("SELECT * FROM todos_task_manifest_bindings WHERE tenant_id = $1 AND operation_id = $2 LIMIT 1 FOR UPDATE", [this.tenantId, manifest.operation_id]);
26442
27300
  if (existing.rows[0]) {
26443
27301
  const binding = existing.rows[0];
26444
- if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest) {
26445
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Operation is already bound to a different request");
27302
+ if (binding["idempotency_key"] !== manifest.idempotency_key || binding["request_digest"] !== input.request_digest || binding["step_id"] !== manifest.step_id || binding["precondition_digest"] !== manifest.precondition_digest) {
27303
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT");
26446
27304
  }
26447
27305
  if (binding["state"] !== "applied") {
26448
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", "Operation was already compensated");
27306
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
26449
27307
  }
26450
- return { ...parseJson(binding["result_json"]), duplicate: true };
27308
+ return parseApplyResult(binding["result_json"], true);
26451
27309
  }
26452
27310
  const reused = await tx.query("SELECT operation_id FROM todos_task_manifest_bindings WHERE tenant_id = $1 AND idempotency_key = $2 LIMIT 1", [this.tenantId, manifest.idempotency_key]);
26453
27311
  if (reused.rows[0])
26454
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Idempotency key is already used");
27312
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT");
27313
+ if (manifest.idempotency_key !== input.expected_idempotency_key) {
27314
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_IDEMPOTENCY_MISMATCH");
27315
+ }
26455
27316
  if (manifest.if_binding_version !== undefined && manifest.if_binding_version !== 0) {
26456
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "New manifest binding version must be 0");
27317
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_CAS_CONFLICT");
26457
27318
  }
26458
27319
  const project = await tx.query(`SELECT 1 AS found FROM ${this.tableName}
26459
27320
  WHERE service = $1 AND object_type = 'projects' AND object_id = $2 AND deleted_at IS NULL LIMIT 1`, [this.service, manifest.project_id]);
26460
27321
  if (!project.rows[0])
26461
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_FOREIGN_REFERENCE", "Project does not exist");
27322
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
26462
27323
  if (manifest.task_list_id) {
26463
27324
  const taskList = await tx.query(`SELECT payload FROM ${this.tableName}
26464
27325
  WHERE service = $1 AND object_type = 'task_lists' AND object_id = $2 AND deleted_at IS NULL LIMIT 1`, [this.service, manifest.task_list_id]);
26465
27326
  const payload = taskList.rows[0] ? parseJson(taskList.rows[0]["payload"]) : null;
26466
27327
  if (!payload || payload["project_id"] !== manifest.project_id) {
26467
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_FOREIGN_REFERENCE", "Task list does not belong to the project");
27328
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_FOREIGN_REFERENCE");
26468
27329
  }
26469
27330
  }
26470
27331
  const objectIds = [input.graph.plan_id, ...Object.values(input.graph.task_ids), ...input.graph.comment_ids, ...input.graph.verification_ids, ...input.graph.dependency_ids];
26471
27332
  const conflict = await tx.query(`SELECT object_id FROM ${this.tableName}
26472
27333
  WHERE service = $1 AND object_id IN (${placeholders(2, objectIds.length)}) LIMIT 1`, [this.service, ...objectIds]);
26473
27334
  if (conflict.rows[0])
26474
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", "A deterministic graph id already exists");
27335
+ return this.persistTerminal(tx, input, "TODOS_TASK_MANIFEST_GRAPH_CONFLICT");
26475
27336
  await this.insertSync(tx, "plans", input.graph.plan_id, planPayload(input), input.now);
26476
27337
  fault(faults, "after_plan_write");
26477
27338
  for (const task2 of manifest.tasks) {
@@ -26541,9 +27402,14 @@ class PostgresTodosTaskManifestBackend {
26541
27402
  schema_version: 1,
26542
27403
  kind: "apply",
26543
27404
  operation_id: manifest.operation_id,
27405
+ step_id: manifest.step_id,
26544
27406
  idempotency_key: manifest.idempotency_key,
26545
27407
  request_digest: input.request_digest,
27408
+ precondition_digest: manifest.precondition_digest,
26546
27409
  result_digest: input.result_digest,
27410
+ outcome: "accepted",
27411
+ reason: null,
27412
+ duplicate_of_receipt_id: null,
26547
27413
  binding_version: 1,
26548
27414
  apply_receipt_id: null,
26549
27415
  created_at: input.now
@@ -26560,14 +27426,18 @@ class PostgresTodosTaskManifestBackend {
26560
27426
  const resultJson = canonicalJson(result);
26561
27427
  await tx.query(`INSERT INTO todos_task_manifest_receipts (
26562
27428
  receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
26563
- request_digest, result_digest, binding_version, apply_receipt_id, manifest_json, result_json, created_at
26564
- ) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'apply', $3, $4, $5, $6, 1, NULL, $7::jsonb, $8::jsonb, $9)`, [
27429
+ step_id, request_digest, precondition_digest, result_digest, slug_provenance, outcome,
27430
+ reason, duplicate_of_receipt_id, binding_version, apply_receipt_id, manifest_json, result_json, created_at
27431
+ ) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'apply', $3, $4, $5, $6, $7, $8, $9, 'accepted', NULL, NULL, 1, NULL, $10::jsonb, $11::jsonb, $12)`, [
26565
27432
  input.receipt_id,
26566
27433
  this.tenantId,
26567
27434
  manifest.operation_id,
26568
27435
  manifest.idempotency_key,
27436
+ manifest.step_id,
26569
27437
  input.request_digest,
27438
+ manifest.precondition_digest,
26570
27439
  input.result_digest,
27440
+ TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE,
26571
27441
  manifestJson,
26572
27442
  resultJson,
26573
27443
  input.now
@@ -26586,14 +27456,18 @@ class PostgresTodosTaskManifestBackend {
26586
27456
  }
26587
27457
  fault(faults, "after_outbox_write");
26588
27458
  await tx.query(`INSERT INTO todos_task_manifest_bindings (
26589
- operation_id, tenant_id, idempotency_key, request_digest, result_digest, apply_receipt_id,
26590
- manifest_json, result_json, state, version, created_at, updated_at
26591
- ) VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb, $8::jsonb, 'applied', 1, $9, $9)`, [
27459
+ operation_id, tenant_id, step_id, idempotency_key, request_digest, precondition_digest,
27460
+ result_digest, slug_provenance, outcome, apply_receipt_id, manifest_json, result_json,
27461
+ state, version, created_at, updated_at
27462
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, 'accepted', $9, $10::jsonb, $11::jsonb, 'applied', 1, $12, $12)`, [
26592
27463
  manifest.operation_id,
26593
27464
  this.tenantId,
27465
+ manifest.step_id,
26594
27466
  manifest.idempotency_key,
26595
27467
  input.request_digest,
27468
+ manifest.precondition_digest,
26596
27469
  input.result_digest,
27470
+ TASK_MANIFEST_DETERMINISTIC_SLUG_PROVENANCE,
26597
27471
  input.receipt_id,
26598
27472
  manifestJson,
26599
27473
  resultJson,
@@ -26606,9 +27480,12 @@ class PostgresTodosTaskManifestBackend {
26606
27480
  async readExact(receiptId2) {
26607
27481
  await this.ensureSchema();
26608
27482
  const result = await this.client.query("SELECT result_json FROM todos_task_manifest_receipts WHERE tenant_id = $1 AND receipt_id = $2 AND kind = 'apply' LIMIT 1", [this.tenantId, receiptId2]);
26609
- if (!result.rows[0])
27483
+ if (result.rows[0])
27484
+ return parseApplyResult(result.rows[0]["result_json"], false);
27485
+ const terminal = await this.client.query("SELECT result_json FROM todos_task_manifest_terminal_receipts WHERE tenant_id = $1 AND receipt_id = $2 AND kind = 'apply' LIMIT 1", [this.tenantId, receiptId2]);
27486
+ if (!terminal.rows[0])
26610
27487
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_RECEIPT_NOT_FOUND", `Apply receipt not found: ${receiptId2}`);
26611
- return { ...parseJson(result.rows[0]["result_json"]), duplicate: false };
27488
+ return parseApplyResult(terminal.rows[0]["result_json"], false);
26612
27489
  }
26613
27490
  async lookupBindingByPlanId(planId) {
26614
27491
  await this.ensureSchema();
@@ -26619,6 +27496,7 @@ class PostgresTodosTaskManifestBackend {
26619
27496
  b.version AS binding_version,
26620
27497
  b.tenant_id AS binding_tenant_id,
26621
27498
  b.operation_id AS binding_operation_id,
27499
+ b.step_id AS binding_step_id,
26622
27500
  b.result_json #>> '{graph,plan_id}' AS binding_plan_id,
26623
27501
  r.tenant_id AS receipt_tenant_id,
26624
27502
  r.authority AS receipt_authority,
@@ -26626,6 +27504,7 @@ class PostgresTodosTaskManifestBackend {
26626
27504
  r.schema_version AS receipt_schema_version,
26627
27505
  r.kind AS receipt_kind,
26628
27506
  r.operation_id AS receipt_operation_id,
27507
+ r.step_id AS receipt_step_id,
26629
27508
  r.result_json #>> '{graph,plan_id}' AS receipt_plan_id
26630
27509
  FROM todos_task_manifest_bindings b
26631
27510
  LEFT JOIN todos_task_manifest_receipts r
@@ -26712,6 +27591,10 @@ class PostgresTodosTaskManifestBackend {
26712
27591
  if (!binding || Number(binding["version"]) !== input.if_binding_version) {
26713
27592
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_CAS_CONFLICT", "Binding version changed before compensation");
26714
27593
  }
27594
+ const appliedReceipt = receiptFromRow3(applyRow);
27595
+ if (appliedReceipt.receipt_id !== input.receipt_id || appliedReceipt.operation_id !== input.operation_id || String(binding["operation_id"]) !== appliedReceipt.operation_id || String(binding["step_id"] ?? "legacy-apply") !== appliedReceipt.step_id || String(binding["idempotency_key"]) !== appliedReceipt.idempotency_key || String(binding["request_digest"]) !== appliedReceipt.request_digest || String(binding["precondition_digest"] ?? "0".repeat(64)) !== appliedReceipt.precondition_digest || String(binding["apply_receipt_id"]) !== input.receipt_id || binding["slug_provenance"] !== applyRow["slug_provenance"]) {
27596
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: receipt and binding identity disagree");
27597
+ }
26715
27598
  if (binding["state"] !== "applied")
26716
27599
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not applied");
26717
27600
  const delivered = await tx.query(`SELECT o.id FROM todos_task_manifest_outbox o
@@ -26726,12 +27609,18 @@ class PostgresTodosTaskManifestBackend {
26726
27609
  LIMIT 1`, [this.tenantId, input.receipt_id]);
26727
27610
  if (delivered.rows[0])
26728
27611
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
26729
- const applyResult = parseJson(applyRow["result_json"]);
27612
+ const applyResult = parseApplyResult(applyRow["result_json"], false);
26730
27613
  const manifest = parseJson(applyRow["manifest_json"]);
27614
+ const manifestRecord = manifest;
27615
+ const applyStepId = typeof manifestRecord["step_id"] === "string" ? String(manifestRecord["step_id"]) : null;
26731
27616
  const expectedEffects = [
26732
27617
  {
26733
27618
  topic: "todos.task-manifest.applied",
26734
- payload: { operation_id: manifest.operation_id, project_id: manifest.project_id }
27619
+ payload: {
27620
+ operation_id: manifest.operation_id,
27621
+ ...applyStepId ? { step_id: applyStepId } : {},
27622
+ project_id: manifest.project_id
27623
+ }
26735
27624
  },
26736
27625
  ...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
26737
27626
  ];
@@ -26779,9 +27668,15 @@ class PostgresTodosTaskManifestBackend {
26779
27668
  }
26780
27669
  const appliedAt = receiptFromRow3(applyRow).created_at;
26781
27670
  const expectedPayloads = new Map;
27671
+ const planRow = await tx.query(`SELECT payload FROM ${this.tableName}
27672
+ WHERE service = $1 AND object_type = 'plans' AND object_id = $2
27673
+ LIMIT 1`, [this.service, applyResult.graph.plan_id]);
27674
+ const actualPlan = planRow.rows[0] ? parseJson(planRow.rows[0]["payload"]) : null;
27675
+ const planExpected = planPayload({ manifest, graph: applyResult.graph, now: appliedAt });
27676
+ planExpected.slug = validatePostgresPlanSlug(manifest, applyResult.graph.plan_id, actualPlan?.["slug"], applyRow["slug_provenance"]);
26782
27677
  expectedPayloads.set(applyResult.graph.plan_id, {
26783
27678
  type: "plans",
26784
- payload: canonicalJson(planPayload({ manifest, graph: applyResult.graph, now: appliedAt }))
27679
+ payload: canonicalJson(planExpected)
26785
27680
  });
26786
27681
  for (const task2 of manifest.tasks)
26787
27682
  expectedPayloads.set(applyResult.graph.task_ids[task2.key], {
@@ -26881,14 +27776,17 @@ class PostgresTodosTaskManifestBackend {
26881
27776
  const readback = await this.readback(tx, applyResult.graph);
26882
27777
  const result = { duplicate: false, receipt, absent: true, readback };
26883
27778
  await tx.query(`INSERT INTO todos_task_manifest_receipts (
26884
- receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, idempotency_key,
26885
- request_digest, result_digest, binding_version, apply_receipt_id, manifest_json, result_json, created_at
26886
- ) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'compensate', $3, $4, $5, $6, $7, $8, NULL, $9::jsonb, $10)`, [
27779
+ receipt_id, tenant_id, authority, route, schema_version, kind, operation_id, step_id, idempotency_key,
27780
+ request_digest, precondition_digest, result_digest, slug_provenance, outcome, reason,
27781
+ duplicate_of_receipt_id, binding_version, apply_receipt_id, manifest_json, result_json, created_at
27782
+ ) VALUES ($1, $2, 'todos', 'todos.task-manifest.v1', 1, 'compensate', $3, $4, $5, $6, $7, $8, NULL, 'accepted', NULL, NULL, $9, $10, NULL, $11::jsonb, $12)`, [
26887
27783
  compensationReceiptId,
26888
27784
  this.tenantId,
26889
27785
  receipt.operation_id,
27786
+ receipt.step_id,
26890
27787
  input.idempotency_key,
26891
27788
  requestDigest,
27789
+ input.precondition_digest,
26892
27790
  receipt.result_digest,
26893
27791
  receipt.binding_version,
26894
27792
  input.receipt_id,
@@ -26945,36 +27843,83 @@ function resolveTenantId(value) {
26945
27843
  }
26946
27844
  return tenantId;
26947
27845
  }
27846
+ function taskManifestRequestDigest(manifest) {
27847
+ const { idempotency_key: _idempotencyKey, ...request } = manifest;
27848
+ return canonicalDigest(request);
27849
+ }
27850
+ function taskManifestCompensationRequestDigest(request) {
27851
+ return canonicalDigest(request);
27852
+ }
27853
+ function deriveTodosTaskManifestApplyPreconditionDigest(input) {
27854
+ return canonicalDigest({
27855
+ route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
27856
+ direction: "apply",
27857
+ operation_id: input.operation_id,
27858
+ step_id: input.step_id,
27859
+ project_id: input.project_id,
27860
+ task_list_id: input.task_list_id ?? null,
27861
+ expected_binding_version: input.if_binding_version ?? 0
27862
+ });
27863
+ }
27864
+ function deriveTodosTaskManifestCompensationPreconditionDigest(input) {
27865
+ return canonicalDigest({
27866
+ route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
27867
+ direction: "compensate",
27868
+ operation_id: input.operation_id,
27869
+ step_id: input.step_id,
27870
+ apply_receipt_id: input.receipt_id,
27871
+ expected_binding_version: input.if_binding_version
27872
+ });
27873
+ }
27874
+ function deriveTodosTaskManifestIdempotencyKey(input) {
27875
+ return `tmk_${canonicalDigest({
27876
+ route: TODOS_TASK_MANIFEST_CALLER_ROUTE,
27877
+ ...input
27878
+ }).slice(0, 48)}`;
27879
+ }
26948
27880
  function normalize(input, now3) {
26949
27881
  const parsed = parseTodosTaskManifest(input);
26950
27882
  const requestBytes = Buffer.byteLength(canonicalJson(parsed), "utf8");
26951
27883
  if (requestBytes > TODOS_TASK_MANIFEST_BOUNDS.request_bytes) {
26952
27884
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_BOUNDS_EXCEEDED", `Task manifest requires ${requestBytes} bytes but the bound is ${TODOS_TASK_MANIFEST_BOUNDS.request_bytes}`, { request_bytes: requestBytes, request_byte_limit: TODOS_TASK_MANIFEST_BOUNDS.request_bytes });
26953
27885
  }
27886
+ const { idempotency_key: _idempotencyKey, ...request } = parsed;
27887
+ const request_digest = taskManifestRequestDigest(request);
26954
27888
  const manifest = sanitizeManifest(parsed);
27889
+ const expectedPreconditionDigest = deriveTodosTaskManifestApplyPreconditionDigest(manifest);
27890
+ if (manifest.precondition_digest !== expectedPreconditionDigest) {
27891
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_DIGEST_MISMATCH", "precondition_digest does not match the exact apply target and binding version", { expected_precondition_digest: expectedPreconditionDigest });
27892
+ }
27893
+ const expectedIdempotencyKey = deriveTodosTaskManifestIdempotencyKey({
27894
+ operation_id: manifest.operation_id,
27895
+ step_id: manifest.step_id,
27896
+ direction: "apply",
27897
+ target_selector: manifest.project_id,
27898
+ request_digest,
27899
+ precondition_digest: manifest.precondition_digest
27900
+ });
26955
27901
  const task_ids = Object.fromEntries(manifest.tasks.map((task2) => [
26956
27902
  task2.key,
26957
- deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "task", task2.key)
27903
+ deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "task", task2.key)
26958
27904
  ]));
26959
27905
  const graph = {
26960
- plan_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "plan", manifest.plan.key),
27906
+ plan_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "plan", manifest.plan.key),
26961
27907
  task_ids,
26962
- comment_ids: manifest.tasks.flatMap((task2) => (task2.comments ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "comment", task2.key, String(index)))),
26963
- verification_ids: manifest.tasks.flatMap((task2) => (task2.verifications ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "verification", task2.key, String(index)))),
27908
+ comment_ids: manifest.tasks.flatMap((task2) => (task2.comments ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "comment", task2.key, String(index)))),
27909
+ verification_ids: manifest.tasks.flatMap((task2) => (task2.verifications ?? []).map((_, index) => deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "verification", task2.key, String(index)))),
26964
27910
  dependency_ids: (manifest.dependencies ?? []).map((edge) => `${task_ids[edge.task]}::${task_ids[edge.depends_on]}`)
26965
27911
  };
26966
- const request_digest = canonicalDigest(parsed);
26967
27912
  const effectInputs = [
26968
27913
  {
26969
27914
  topic: "todos.task-manifest.applied",
26970
- payload: { operation_id: manifest.operation_id, project_id: manifest.project_id }
27915
+ payload: { operation_id: manifest.operation_id, step_id: manifest.step_id, project_id: manifest.project_id }
26971
27916
  },
26972
27917
  ...manifest.effects ?? []
26973
27918
  ];
26974
27919
  const outbox = effectInputs.map((effect2, index) => {
26975
27920
  const payload = { ...effect2.payload };
26976
27921
  return {
26977
- id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, "outbox", String(index)),
27922
+ id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, manifest.operation_id, manifest.step_id, "outbox", String(index)),
26978
27923
  topic: effect2.topic,
26979
27924
  payload,
26980
27925
  digest: canonicalDigest({ topic: effect2.topic, payload })
@@ -26984,11 +27929,14 @@ function normalize(input, now3) {
26984
27929
  return {
26985
27930
  manifest,
26986
27931
  request_digest,
27932
+ expected_idempotency_key: expectedIdempotencyKey,
26987
27933
  result_digest,
26988
- receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "apply", manifest.operation_id, manifest.idempotency_key, request_digest),
27934
+ receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "apply", manifest.operation_id, manifest.step_id, manifest.idempotency_key, request_digest),
27935
+ terminal_receipt_id: deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "terminal", "apply", manifest.operation_id, manifest.step_id, manifest.idempotency_key, request_digest),
26989
27936
  graph,
26990
27937
  outbox,
26991
- now: now3
27938
+ now: now3,
27939
+ plan_slug_provenance: TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE
26992
27940
  };
26993
27941
  }
26994
27942
  function sanitizeManifest(manifest) {
@@ -27046,6 +27994,10 @@ class PackageOwnedTodosTaskManifestAuthority {
27046
27994
  tenant_id: this.tenantId,
27047
27995
  backend: this.backend.kind,
27048
27996
  deterministic_ids: true,
27997
+ operation_step_identity: true,
27998
+ deterministic_idempotency_keys: true,
27999
+ terminal_nonacceptance_receipts: true,
28000
+ plan_slug_provenance: TODOS_TASK_MANIFEST_PLAN_SLUG_PROVENANCE,
27049
28001
  immutable_receipts: true,
27050
28002
  transactional_outbox: true,
27051
28003
  idempotent_outbox_delivery: true,
@@ -27075,7 +28027,11 @@ class PackageOwnedTodosTaskManifestAuthority {
27075
28027
  async apply(input) {
27076
28028
  const normalized = normalize(input, this.now());
27077
28029
  const faults = await this.prepareFaults();
27078
- return this.bounded(await this.backend.apply(normalized, faults));
28030
+ const result = this.bounded(await this.backend.apply(normalized, faults));
28031
+ if (result.receipt.outcome === "terminal_nonacceptance") {
28032
+ throw new TodosTaskManifestError(result.receipt.reason ?? "TODOS_TASK_MANIFEST_GRAPH_CONFLICT", "Task-manifest apply reached an immutable terminal nonacceptance", { receipt: result.receipt });
28033
+ }
28034
+ return result;
27079
28035
  }
27080
28036
  readExact(receiptId2) {
27081
28037
  if (!receiptId2 || receiptId2.length > 200) {
@@ -27108,18 +28064,48 @@ class PackageOwnedTodosTaskManifestAuthority {
27108
28064
  async compensate(input) {
27109
28065
  const request = parseTodosTaskManifestCompensation(input);
27110
28066
  const applied = await this.backend.readExact(request.receipt_id);
27111
- const requestDigest = canonicalDigest(request);
27112
- const compensationReceiptId = deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "compensate", applied.receipt.operation_id, request.idempotency_key, requestDigest);
28067
+ if (applied.receipt.outcome !== "accepted") {
28068
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: apply receipt is terminal nonacceptance");
28069
+ }
28070
+ if (request.operation_id !== applied.receipt.operation_id) {
28071
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_CONFLICT", "Compensation operation_id must match the accepted apply operation");
28072
+ }
28073
+ if (request.step_id === applied.receipt.step_id) {
28074
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_INVALID_INPUT", "Compensation must use a distinct step_id from apply");
28075
+ }
28076
+ const expectedPreconditionDigest = deriveTodosTaskManifestCompensationPreconditionDigest(request);
28077
+ if (request.precondition_digest !== expectedPreconditionDigest) {
28078
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_DIGEST_MISMATCH", "precondition_digest does not match the exact compensation receipt and binding version", { expected_precondition_digest: expectedPreconditionDigest });
28079
+ }
28080
+ const { idempotency_key: _requestIdempotencyKey, ...compensationRequestWithoutKey } = request;
28081
+ const requestDigest = taskManifestCompensationRequestDigest(compensationRequestWithoutKey);
28082
+ const expectedIdempotencyKey = deriveTodosTaskManifestIdempotencyKey({
28083
+ operation_id: request.operation_id,
28084
+ step_id: request.step_id,
28085
+ direction: "compensate",
28086
+ target_selector: request.receipt_id,
28087
+ request_digest: requestDigest,
28088
+ precondition_digest: request.precondition_digest
28089
+ });
28090
+ if (request.idempotency_key !== expectedIdempotencyKey) {
28091
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_IDEMPOTENCY_MISMATCH", "idempotency_key does not match the deterministic operation/step/compensation semantics", { expected_idempotency_key: expectedIdempotencyKey });
28092
+ }
28093
+ const compensationReceiptId = deterministicUuid(TODOS_TASK_MANIFEST_ROUTE, "compensate", request.operation_id, request.step_id, request.idempotency_key, requestDigest);
27113
28094
  const receipt = {
27114
28095
  receipt_id: compensationReceiptId,
27115
28096
  authority: "todos",
27116
28097
  route: TODOS_TASK_MANIFEST_ROUTE,
27117
28098
  schema_version: 1,
27118
28099
  kind: "compensate",
27119
- operation_id: applied.receipt.operation_id,
28100
+ operation_id: request.operation_id,
28101
+ step_id: request.step_id,
27120
28102
  idempotency_key: request.idempotency_key,
27121
28103
  request_digest: requestDigest,
28104
+ precondition_digest: request.precondition_digest,
27122
28105
  result_digest: canonicalDigest({ absent: true, apply_receipt_id: applied.receipt.receipt_id }),
28106
+ outcome: "accepted",
28107
+ reason: null,
28108
+ duplicate_of_receipt_id: null,
27123
28109
  binding_version: request.if_binding_version + 1,
27124
28110
  apply_receipt_id: applied.receipt.receipt_id,
27125
28111
  created_at: this.now()
@@ -29624,9 +30610,24 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
29624
30610
  TaskManifestBounds: taskManifestBoundsSchema,
29625
30611
  TaskManifestCapability: taskManifestCapabilitySchema,
29626
30612
  TaskManifestCapabilityResponse: taskManifestCapabilityResponseSchema,
30613
+ TaskManifest: taskManifestSchema,
30614
+ TaskManifestReceipt: taskManifestReceiptSchema,
30615
+ TaskManifestApplyResult: taskManifestApplyResultSchema,
30616
+ TaskManifestApplyResponse: taskManifestApplyResponseSchema,
30617
+ TaskManifestCompensateRequest: taskManifestCompensateRequestSchema,
30618
+ TaskManifestCompensationResult: taskManifestCompensationResultSchema,
30619
+ TaskManifestCompensateResponse: taskManifestCompensateResponseSchema,
30620
+ TaskManifestReadExactRequest: taskManifestReadExactRequestSchema,
29627
30621
  TaskManifestBindingLookupRequest: taskManifestBindingLookupRequestSchema,
29628
30622
  TaskManifestBindingLookupResult: taskManifestBindingLookupResultSchema,
29629
30623
  TaskManifestBindingLookupResponse: taskManifestBindingLookupResponseSchema,
30624
+ ProjectRegistrationCapability: projectRegistrationCapabilitySchema,
30625
+ ProjectRegistrationReceipt: projectRegistrationReceiptSchema,
30626
+ ProjectRegistrationRequest: projectRegistrationRequestSchema,
30627
+ ProjectRegistrationLookupRequest: projectRegistrationLookupRequestSchema,
30628
+ PriorRegistrationAdoptionValidation: priorRegistrationAdoptionValidationSchema,
30629
+ ProjectResource: projectResourceSchema,
30630
+ ProjectResourcePage: projectResourcePageSchema,
29630
30631
  TaskList: taskListSchema,
29631
30632
  ProjectTaskListEnsureReceipt: projectTaskListEnsureReceiptSchema,
29632
30633
  ProjectTaskListEnsureResult: projectTaskListEnsureResultSchema,
@@ -29669,6 +30670,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
29669
30670
  priority: { type: "string", enum: [...TASK_PRIORITIES] },
29670
30671
  assigned_to: { type: "string" },
29671
30672
  project_id: { type: "string", nullable: true },
30673
+ parent_id: { type: "string", nullable: true },
29672
30674
  plan_id: { type: "string", nullable: true },
29673
30675
  task_list_id: { type: "string", nullable: true },
29674
30676
  version: { type: "number" }
@@ -30623,6 +31625,341 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
30623
31625
  },
30624
31626
  security: [{ apiKey: [] }],
30625
31627
  paths: {
31628
+ "/v1/project-registration/capability": {
31629
+ get: {
31630
+ operationId: "getProjectRegistrationCapability",
31631
+ summary: "Read the live package-owned Projects to Todos registration capability",
31632
+ responses: {
31633
+ "200": {
31634
+ content: {
31635
+ "application/json": {
31636
+ schema: {
31637
+ type: "object",
31638
+ additionalProperties: false,
31639
+ required: ["capability"],
31640
+ properties: {
31641
+ capability: {
31642
+ $ref: "#/components/schemas/ProjectRegistrationCapability"
31643
+ }
31644
+ }
31645
+ }
31646
+ }
31647
+ }
31648
+ }
31649
+ }
31650
+ }
31651
+ },
31652
+ "/v1/project-registration/resources": {
31653
+ get: {
31654
+ operationId: "listProjectRegistrationResources",
31655
+ summary: "List one bounded page of stable Todos identities for an exact Projects workspace id",
31656
+ parameters: [
31657
+ {
31658
+ name: "source_project_id",
31659
+ in: "query",
31660
+ required: true,
31661
+ schema: { type: "string" }
31662
+ },
31663
+ {
31664
+ name: "include_anchors",
31665
+ in: "query",
31666
+ schema: { type: "boolean", default: false }
31667
+ },
31668
+ {
31669
+ name: "limit",
31670
+ in: "query",
31671
+ schema: { type: "integer", minimum: 1, maximum: 500, default: 100 }
31672
+ },
31673
+ {
31674
+ name: "cursor",
31675
+ in: "query",
31676
+ schema: { type: "string" }
31677
+ }
31678
+ ],
31679
+ responses: {
31680
+ "200": {
31681
+ content: {
31682
+ "application/json": {
31683
+ schema: {
31684
+ type: "object",
31685
+ additionalProperties: false,
31686
+ required: ["page"],
31687
+ properties: {
31688
+ page: { $ref: "#/components/schemas/ProjectResourcePage" }
31689
+ }
31690
+ }
31691
+ }
31692
+ }
31693
+ },
31694
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
31695
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
31696
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
31697
+ }
31698
+ }
31699
+ },
31700
+ "/v1/project-registration/create": {
31701
+ post: {
31702
+ operationId: "createProjectRegistrationResource",
31703
+ summary: "Create or deterministically bind one Projects to Todos resource",
31704
+ requestBody: {
31705
+ required: true,
31706
+ content: {
31707
+ "application/json": {
31708
+ schema: { $ref: "#/components/schemas/ProjectRegistrationRequest" }
31709
+ }
31710
+ }
31711
+ },
31712
+ responses: {
31713
+ "201": {
31714
+ content: {
31715
+ "application/json": {
31716
+ schema: {
31717
+ type: "object",
31718
+ additionalProperties: false,
31719
+ required: ["receipt"],
31720
+ properties: {
31721
+ receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" }
31722
+ }
31723
+ }
31724
+ }
31725
+ }
31726
+ },
31727
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
31728
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
31729
+ }
31730
+ }
31731
+ },
31732
+ "/v1/project-registration/read-exact": {
31733
+ post: {
31734
+ operationId: "readExactProjectRegistrationResource",
31735
+ summary: "Read one registered project or task list by exact full UUID",
31736
+ requestBody: {
31737
+ required: true,
31738
+ content: {
31739
+ "application/json": {
31740
+ schema: {
31741
+ type: "object",
31742
+ additionalProperties: false,
31743
+ required: [
31744
+ "resource_kind",
31745
+ "target_id",
31746
+ "response_byte_limit",
31747
+ "time_budget_ms"
31748
+ ],
31749
+ properties: {
31750
+ resource_kind: { type: "string", enum: ["project", "task_list"] },
31751
+ target_id: { type: "string", format: "uuid" },
31752
+ ...projectRegistrationBoundsProperties
31753
+ }
31754
+ }
31755
+ }
31756
+ }
31757
+ },
31758
+ responses: {
31759
+ "200": {
31760
+ content: {
31761
+ "application/json": {
31762
+ schema: {
31763
+ type: "object",
31764
+ additionalProperties: false,
31765
+ required: ["record"],
31766
+ properties: {
31767
+ record: {
31768
+ type: "object",
31769
+ additionalProperties: false,
31770
+ required: ["target_id", "revision", "digest"],
31771
+ properties: {
31772
+ target_id: { type: "string", format: "uuid" },
31773
+ revision: { type: "string" },
31774
+ digest: { type: "string" }
31775
+ }
31776
+ }
31777
+ }
31778
+ }
31779
+ }
31780
+ }
31781
+ },
31782
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
31783
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
31784
+ }
31785
+ }
31786
+ },
31787
+ "/v1/project-registration/receipts/lookup": {
31788
+ post: {
31789
+ operationId: "lookupProjectRegistrationReceipt",
31790
+ summary: "Recover one exact immutable terminal registration receipt",
31791
+ requestBody: {
31792
+ required: true,
31793
+ content: {
31794
+ "application/json": {
31795
+ schema: { $ref: "#/components/schemas/ProjectRegistrationLookupRequest" }
31796
+ }
31797
+ }
31798
+ },
31799
+ responses: {
31800
+ "200": {
31801
+ content: {
31802
+ "application/json": {
31803
+ schema: {
31804
+ type: "object",
31805
+ additionalProperties: false,
31806
+ required: ["receipt", "response_control"],
31807
+ properties: {
31808
+ receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" },
31809
+ response_control: {
31810
+ type: "object",
31811
+ additionalProperties: false,
31812
+ required: [
31813
+ "response_byte_limit",
31814
+ "time_budget_ms",
31815
+ "response_bytes",
31816
+ "elapsed_ms",
31817
+ "complete",
31818
+ "truncated"
31819
+ ],
31820
+ properties: {
31821
+ ...projectRegistrationBoundsProperties,
31822
+ response_bytes: { type: "integer", minimum: 0 },
31823
+ elapsed_ms: { type: "integer", minimum: 0 },
31824
+ complete: { type: "boolean", enum: [true] },
31825
+ truncated: { type: "boolean", enum: [false] }
31826
+ }
31827
+ }
31828
+ }
31829
+ }
31830
+ }
31831
+ }
31832
+ },
31833
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
31834
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
31835
+ }
31836
+ }
31837
+ },
31838
+ "/v1/project-registration/validate-prior-adoption": {
31839
+ post: {
31840
+ operationId: "validatePriorRegistrationAdoption",
31841
+ summary: "Fail closed unless one prior accepted registration still matches its exact current resource",
31842
+ requestBody: {
31843
+ required: true,
31844
+ content: {
31845
+ "application/json": {
31846
+ schema: {
31847
+ type: "object",
31848
+ additionalProperties: false,
31849
+ required: ["source_request", "source_receipt", "current_record"],
31850
+ properties: {
31851
+ source_request: { $ref: "#/components/schemas/ProjectRegistrationRequest" },
31852
+ source_receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" },
31853
+ current_record: {
31854
+ oneOf: [
31855
+ { $ref: "#/components/schemas/Project" },
31856
+ { $ref: "#/components/schemas/TaskList" }
31857
+ ]
31858
+ }
31859
+ }
31860
+ }
31861
+ }
31862
+ }
31863
+ },
31864
+ responses: {
31865
+ "200": {
31866
+ content: {
31867
+ "application/json": {
31868
+ schema: {
31869
+ type: "object",
31870
+ additionalProperties: false,
31871
+ required: ["validation"],
31872
+ properties: {
31873
+ validation: {
31874
+ $ref: "#/components/schemas/PriorRegistrationAdoptionValidation"
31875
+ }
31876
+ }
31877
+ }
31878
+ }
31879
+ }
31880
+ },
31881
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
31882
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
31883
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
31884
+ }
31885
+ }
31886
+ },
31887
+ "/v1/project-registration/compensate": {
31888
+ post: {
31889
+ operationId: "compensateProjectRegistrationResource",
31890
+ summary: "Conditionally remove an unchanged receipt-owned registration resource",
31891
+ requestBody: {
31892
+ required: true,
31893
+ content: {
31894
+ "application/json": {
31895
+ schema: { $ref: "#/components/schemas/ProjectRegistrationRequest" }
31896
+ }
31897
+ }
31898
+ },
31899
+ responses: {
31900
+ "201": {
31901
+ content: {
31902
+ "application/json": {
31903
+ schema: {
31904
+ type: "object",
31905
+ additionalProperties: false,
31906
+ required: ["receipt"],
31907
+ properties: {
31908
+ receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" }
31909
+ }
31910
+ }
31911
+ }
31912
+ }
31913
+ },
31914
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
31915
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
31916
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
31917
+ }
31918
+ }
31919
+ },
31920
+ "/v1/project-registration/verify-inverse": {
31921
+ post: {
31922
+ operationId: "verifyInverseProjectRegistrationResource",
31923
+ summary: "Verify exact absence after conditional registration compensation",
31924
+ requestBody: {
31925
+ required: true,
31926
+ content: {
31927
+ "application/json": {
31928
+ schema: { $ref: "#/components/schemas/ProjectRegistrationRequest" }
31929
+ }
31930
+ }
31931
+ },
31932
+ responses: {
31933
+ "200": {
31934
+ content: {
31935
+ "application/json": {
31936
+ schema: {
31937
+ type: "object",
31938
+ additionalProperties: false,
31939
+ required: ["verification"],
31940
+ properties: {
31941
+ verification: {
31942
+ type: "object",
31943
+ additionalProperties: false,
31944
+ required: ["target_id", "accepted_receipt_id", "absent", "digest"],
31945
+ properties: {
31946
+ target_id: { type: "string", format: "uuid" },
31947
+ accepted_receipt_id: { type: "string" },
31948
+ absent: { type: "boolean", enum: [true] },
31949
+ digest: { type: "string" }
31950
+ }
31951
+ }
31952
+ }
31953
+ }
31954
+ }
31955
+ }
31956
+ },
31957
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
31958
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
31959
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
31960
+ }
31961
+ }
31962
+ },
30626
31963
  "/v1/task-manifest/capability": {
30627
31964
  get: {
30628
31965
  operationId: "getTaskManifestCapability",
@@ -30640,6 +31977,82 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
30640
31977
  }
30641
31978
  }
30642
31979
  },
31980
+ "/v1/task-manifest/apply": {
31981
+ post: {
31982
+ operationId: "applyTaskManifest",
31983
+ summary: "Apply one exact task-manifest graph through the Todos authority",
31984
+ requestBody: {
31985
+ required: true,
31986
+ content: {
31987
+ "application/json": {
31988
+ schema: { $ref: "#/components/schemas/TaskManifest" }
31989
+ }
31990
+ }
31991
+ },
31992
+ responses: {
31993
+ "201": {
31994
+ content: {
31995
+ "application/json": {
31996
+ schema: { $ref: "#/components/schemas/TaskManifestApplyResponse" }
31997
+ }
31998
+ }
31999
+ },
32000
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
32001
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
32002
+ "503": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
32003
+ }
32004
+ }
32005
+ },
32006
+ "/v1/task-manifest/read-exact": {
32007
+ post: {
32008
+ operationId: "readExactTaskManifest",
32009
+ summary: "Read one exact immutable task-manifest apply receipt",
32010
+ requestBody: {
32011
+ required: true,
32012
+ content: {
32013
+ "application/json": {
32014
+ schema: { $ref: "#/components/schemas/TaskManifestReadExactRequest" }
32015
+ }
32016
+ }
32017
+ },
32018
+ responses: {
32019
+ "200": {
32020
+ content: {
32021
+ "application/json": {
32022
+ schema: { $ref: "#/components/schemas/TaskManifestApplyResponse" }
32023
+ }
32024
+ }
32025
+ },
32026
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
32027
+ }
32028
+ }
32029
+ },
32030
+ "/v1/task-manifest/compensate": {
32031
+ post: {
32032
+ operationId: "compensateTaskManifest",
32033
+ summary: "Compensate one exact untouched task-manifest graph with CAS protection",
32034
+ requestBody: {
32035
+ required: true,
32036
+ content: {
32037
+ "application/json": {
32038
+ schema: { $ref: "#/components/schemas/TaskManifestCompensateRequest" }
32039
+ }
32040
+ }
32041
+ },
32042
+ responses: {
32043
+ "201": {
32044
+ content: {
32045
+ "application/json": {
32046
+ schema: { $ref: "#/components/schemas/TaskManifestCompensateResponse" }
32047
+ }
32048
+ }
32049
+ },
32050
+ "400": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
32051
+ "404": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } },
32052
+ "409": { content: { "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } } } }
32053
+ }
32054
+ }
32055
+ },
30643
32056
  "/v1/task-manifest/bindings/lookup": {
30644
32057
  post: {
30645
32058
  operationId: "lookupTaskManifestBinding",
@@ -31500,7 +32913,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
31500
32913
  }
31501
32914
  });
31502
32915
  }
31503
- var taskSchema, taskManifestBoundsSchema, taskManifestCapabilitySchema, taskManifestCapabilityResponseSchema, projectSchema, taskManifestBindingLookupRequestSchema, taskManifestBindingLookupResultSchema, taskManifestBindingLookupResponseSchema, taskListSchema, projectTaskListEnsureReceiptSchema, projectTaskListEnsureResultSchema, projectTaskListRollbackResultSchema, taskCommentSchema, staleLockHandoffInputSchema, staleLockHandoffReceiptSchema, taskGitRefSchema, planSchema, planProjectLinkReceiptSchema, planProjectLinkResultSchema, planProjectLinkRollbackResultSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
32916
+ var taskSchema, taskManifestBoundsSchema, taskManifestCapabilitySchema, taskManifestCapabilityResponseSchema, projectSchema, taskManifestBindingLookupRequestSchema, taskManifestBindingLookupResultSchema, taskManifestSchema, taskManifestReceiptSchema, taskManifestApplyResultSchema, taskManifestApplyResponseSchema, taskManifestCompensateRequestSchema, taskManifestCompensationResultSchema, taskManifestCompensateResponseSchema, taskManifestReadExactRequestSchema, taskManifestBindingLookupResponseSchema, taskListSchema, projectTaskListEnsureReceiptSchema, projectTaskListEnsureResultSchema, projectTaskListRollbackResultSchema, taskCommentSchema, staleLockHandoffInputSchema, staleLockHandoffReceiptSchema, taskGitRefSchema, planSchema, planProjectLinkReceiptSchema, planProjectLinkResultSchema, planProjectLinkRollbackResultSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema, projectRegistrationBoundsProperties, projectRegistrationReceiptSchema, projectRegistrationCapabilitySchema, projectRegistrationRequestSchema, projectRegistrationLookupRequestSchema, priorRegistrationAdoptionValidationSchema, projectResourceSchema, projectResourcePageSchema;
31504
32917
  var init_openapi = __esm(() => {
31505
32918
  init_package_version();
31506
32919
  init_types();
@@ -31562,6 +32975,10 @@ var init_openapi = __esm(() => {
31562
32975
  "tenant_id",
31563
32976
  "backend",
31564
32977
  "deterministic_ids",
32978
+ "operation_step_identity",
32979
+ "deterministic_idempotency_keys",
32980
+ "terminal_nonacceptance_receipts",
32981
+ "plan_slug_provenance",
31565
32982
  "immutable_receipts",
31566
32983
  "transactional_outbox",
31567
32984
  "idempotent_outbox_delivery",
@@ -31577,6 +32994,10 @@ var init_openapi = __esm(() => {
31577
32994
  tenant_id: { type: "string", minLength: 1, maxLength: 200 },
31578
32995
  backend: { type: "string", enum: ["sqlite", "postgresql", "http"] },
31579
32996
  deterministic_ids: { type: "boolean", enum: [true] },
32997
+ operation_step_identity: { type: "boolean", enum: [true] },
32998
+ deterministic_idempotency_keys: { type: "boolean", enum: [true] },
32999
+ terminal_nonacceptance_receipts: { type: "boolean", enum: [true] },
33000
+ plan_slug_provenance: { type: "string", enum: ["deterministic-v1"] },
31580
33001
  immutable_receipts: { type: "boolean", enum: [true] },
31581
33002
  transactional_outbox: { type: "boolean", enum: [true] },
31582
33003
  idempotent_outbox_delivery: { type: "boolean", enum: [true] },
@@ -31630,6 +33051,8 @@ var init_openapi = __esm(() => {
31630
33051
  "schema_version",
31631
33052
  "tenant_id",
31632
33053
  "plan_id",
33054
+ "operation_id",
33055
+ "step_id",
31633
33056
  "apply_receipt_id",
31634
33057
  "binding_version",
31635
33058
  "state"
@@ -31640,11 +33063,169 @@ var init_openapi = __esm(() => {
31640
33063
  schema_version: { type: "integer", enum: [1] },
31641
33064
  tenant_id: { type: "string" },
31642
33065
  plan_id: { type: "string", format: "uuid" },
33066
+ operation_id: { type: "string", minLength: 1, maxLength: 200 },
33067
+ step_id: { type: "string", minLength: 1, maxLength: 200 },
31643
33068
  apply_receipt_id: { type: "string", format: "uuid" },
31644
33069
  binding_version: { type: "integer", minimum: 1 },
31645
33070
  state: { type: "string", enum: ["applied", "compensated"] }
31646
33071
  }
31647
33072
  };
33073
+ taskManifestSchema = {
33074
+ type: "object",
33075
+ additionalProperties: false,
33076
+ required: [
33077
+ "version",
33078
+ "operation_id",
33079
+ "step_id",
33080
+ "idempotency_key",
33081
+ "precondition_digest",
33082
+ "project_id",
33083
+ "plan",
33084
+ "tasks"
33085
+ ],
33086
+ properties: {
33087
+ version: { type: "integer", enum: [1] },
33088
+ operation_id: { type: "string", minLength: 1, maxLength: 200 },
33089
+ step_id: { type: "string", minLength: 1, maxLength: 200 },
33090
+ idempotency_key: { type: "string", pattern: "^tmk_[0-9a-f]{48}$" },
33091
+ precondition_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
33092
+ project_id: { type: "string", format: "uuid" },
33093
+ task_list_id: { type: "string", format: "uuid" },
33094
+ if_binding_version: { type: "integer", minimum: 0 },
33095
+ plan: {
33096
+ type: "object",
33097
+ additionalProperties: false,
33098
+ required: ["key", "name"],
33099
+ properties: {
33100
+ key: { type: "string", minLength: 1, maxLength: 200 },
33101
+ name: { type: "string", minLength: 1, maxLength: 200 },
33102
+ description: { type: "string" },
33103
+ status: { type: "string", enum: ["active", "completed", "archived"] }
33104
+ }
33105
+ },
33106
+ tasks: {
33107
+ type: "array",
33108
+ minItems: 1,
33109
+ items: {
33110
+ type: "object",
33111
+ additionalProperties: false,
33112
+ required: ["key", "title"],
33113
+ properties: {
33114
+ key: { type: "string", minLength: 1, maxLength: 200 },
33115
+ title: { type: "string", minLength: 1, maxLength: 200 },
33116
+ description: { type: "string" },
33117
+ status: { type: "string", enum: ["pending", "in_progress", "completed", "failed", "cancelled"] },
33118
+ priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
33119
+ assigned_to: { type: "string" },
33120
+ created_by: { type: "string" },
33121
+ tags: { type: "array", items: { type: "string" } },
33122
+ metadata: { type: "object", additionalProperties: true },
33123
+ comments: { type: "array", items: { type: "object", additionalProperties: true } },
33124
+ verifications: { type: "array", items: { type: "object", additionalProperties: true } }
33125
+ }
33126
+ }
33127
+ },
33128
+ dependencies: { type: "array", items: { type: "object", additionalProperties: true } },
33129
+ effects: { type: "array", items: { type: "object", additionalProperties: true } }
33130
+ }
33131
+ };
33132
+ taskManifestReceiptSchema = {
33133
+ type: "object",
33134
+ additionalProperties: false,
33135
+ required: [
33136
+ "receipt_id",
33137
+ "authority",
33138
+ "route",
33139
+ "schema_version",
33140
+ "kind",
33141
+ "operation_id",
33142
+ "step_id",
33143
+ "idempotency_key",
33144
+ "request_digest",
33145
+ "precondition_digest",
33146
+ "result_digest",
33147
+ "outcome",
33148
+ "reason",
33149
+ "duplicate_of_receipt_id",
33150
+ "binding_version",
33151
+ "apply_receipt_id",
33152
+ "created_at"
33153
+ ],
33154
+ properties: {
33155
+ receipt_id: { type: "string", format: "uuid" },
33156
+ authority: { type: "string", enum: ["todos"] },
33157
+ route: { type: "string", enum: ["todos.task-manifest.v1"] },
33158
+ schema_version: { type: "integer", enum: [1] },
33159
+ kind: { type: "string", enum: ["apply", "compensate"] },
33160
+ operation_id: { type: "string" },
33161
+ step_id: { type: "string" },
33162
+ idempotency_key: { type: "string" },
33163
+ request_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
33164
+ precondition_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
33165
+ result_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
33166
+ outcome: { type: "string", enum: ["accepted", "duplicate_of_accepted", "terminal_nonacceptance"] },
33167
+ reason: { type: "string", nullable: true },
33168
+ duplicate_of_receipt_id: { type: "string", nullable: true },
33169
+ binding_version: { type: "integer", minimum: 0 },
33170
+ apply_receipt_id: { type: "string", nullable: true },
33171
+ created_at: { type: "string", format: "date-time" }
33172
+ }
33173
+ };
33174
+ taskManifestApplyResultSchema = {
33175
+ type: "object",
33176
+ additionalProperties: false,
33177
+ required: ["duplicate", "receipt", "graph", "readback", "outbox_ids", "result_digest"],
33178
+ properties: {
33179
+ duplicate: { type: "boolean" },
33180
+ receipt: { $ref: "#/components/schemas/TaskManifestReceipt" },
33181
+ graph: { type: "object", additionalProperties: true },
33182
+ readback: { type: "object", additionalProperties: true },
33183
+ outbox_ids: { type: "array", items: { type: "string", format: "uuid" } },
33184
+ result_digest: { type: "string", pattern: "^[0-9a-f]{64}$" }
33185
+ }
33186
+ };
33187
+ taskManifestApplyResponseSchema = {
33188
+ type: "object",
33189
+ additionalProperties: false,
33190
+ required: ["result"],
33191
+ properties: { result: { $ref: "#/components/schemas/TaskManifestApplyResult" } }
33192
+ };
33193
+ taskManifestCompensateRequestSchema = {
33194
+ type: "object",
33195
+ additionalProperties: false,
33196
+ required: ["receipt_id", "operation_id", "step_id", "idempotency_key", "precondition_digest", "if_binding_version"],
33197
+ properties: {
33198
+ receipt_id: { type: "string", format: "uuid" },
33199
+ operation_id: { type: "string" },
33200
+ step_id: { type: "string" },
33201
+ idempotency_key: { type: "string", pattern: "^tmk_[0-9a-f]{48}$" },
33202
+ precondition_digest: { type: "string", pattern: "^[0-9a-f]{64}$" },
33203
+ if_binding_version: { type: "integer", minimum: 1 }
33204
+ }
33205
+ };
33206
+ taskManifestCompensationResultSchema = {
33207
+ type: "object",
33208
+ additionalProperties: false,
33209
+ required: ["duplicate", "receipt", "absent", "readback"],
33210
+ properties: {
33211
+ duplicate: { type: "boolean" },
33212
+ receipt: { $ref: "#/components/schemas/TaskManifestReceipt" },
33213
+ absent: { type: "boolean", enum: [true] },
33214
+ readback: { type: "object", additionalProperties: true }
33215
+ }
33216
+ };
33217
+ taskManifestCompensateResponseSchema = {
33218
+ type: "object",
33219
+ additionalProperties: false,
33220
+ required: ["result"],
33221
+ properties: { result: { $ref: "#/components/schemas/TaskManifestCompensationResult" } }
33222
+ };
33223
+ taskManifestReadExactRequestSchema = {
33224
+ type: "object",
33225
+ additionalProperties: false,
33226
+ required: ["receipt_id"],
33227
+ properties: { receipt_id: { type: "string", format: "uuid" } }
33228
+ };
31648
33229
  taskManifestBindingLookupResponseSchema = {
31649
33230
  type: "object",
31650
33231
  additionalProperties: false,
@@ -31994,6 +33575,292 @@ var init_openapi = __esm(() => {
31994
33575
  metadata: { type: "object", additionalProperties: true }
31995
33576
  }
31996
33577
  };
33578
+ projectRegistrationBoundsProperties = {
33579
+ response_byte_limit: { type: "integer", minimum: 1 },
33580
+ time_budget_ms: { type: "integer", minimum: 1 }
33581
+ };
33582
+ projectRegistrationReceiptSchema = {
33583
+ type: "object",
33584
+ additionalProperties: false,
33585
+ required: [
33586
+ "receipt_id",
33587
+ "authority",
33588
+ "route",
33589
+ "package_version",
33590
+ "authority_id",
33591
+ "tenant_id",
33592
+ "corpus_id",
33593
+ "operation_id",
33594
+ "step_id",
33595
+ "resource_kind",
33596
+ "direction",
33597
+ "idempotency_key",
33598
+ "request_digest",
33599
+ "precondition_digest",
33600
+ "outcome",
33601
+ "reason",
33602
+ "target_id",
33603
+ "result_revision",
33604
+ "result_digest",
33605
+ "duplicate_of_receipt_id",
33606
+ "accepted_receipt_id",
33607
+ "created_by_operation",
33608
+ "created_at"
33609
+ ],
33610
+ properties: {
33611
+ receipt_id: { type: "string" },
33612
+ authority: { type: "string", enum: ["todos"] },
33613
+ route: { type: "string", enum: ["todos.project-registration.v1"] },
33614
+ package_version: { type: "string" },
33615
+ authority_id: { type: "string" },
33616
+ tenant_id: { type: "string" },
33617
+ corpus_id: { type: "string" },
33618
+ operation_id: { type: "string" },
33619
+ step_id: { type: "string" },
33620
+ resource_kind: { type: "string", enum: ["project", "task_list"] },
33621
+ direction: { type: "string", enum: ["forward", "inverse"] },
33622
+ idempotency_key: { type: "string" },
33623
+ request_digest: { type: "string" },
33624
+ precondition_digest: { type: "string" },
33625
+ outcome: {
33626
+ type: "string",
33627
+ enum: ["accepted", "duplicate_of_accepted", "terminal_nonacceptance"]
33628
+ },
33629
+ reason: { type: "string", nullable: true },
33630
+ target_id: { type: "string", format: "uuid", nullable: true },
33631
+ result_revision: { type: "string", nullable: true },
33632
+ result_digest: { type: "string", nullable: true },
33633
+ duplicate_of_receipt_id: { type: "string", nullable: true },
33634
+ accepted_receipt_id: { type: "string", nullable: true },
33635
+ created_by_operation: { type: "boolean" },
33636
+ created_at: { type: "string", format: "date-time" }
33637
+ }
33638
+ };
33639
+ projectRegistrationCapabilitySchema = {
33640
+ type: "object",
33641
+ additionalProperties: false,
33642
+ required: [
33643
+ "authority",
33644
+ "route",
33645
+ "package_version",
33646
+ "authority_id",
33647
+ "tenant_id",
33648
+ "corpus_id",
33649
+ "supported_resources",
33650
+ "conditional_create",
33651
+ "immutable_receipts",
33652
+ "exact_terminal_lookup",
33653
+ "exact_readback",
33654
+ "bind_existing_adoption",
33655
+ "prior_registration_adoption_validation",
33656
+ "project_resource_enumeration",
33657
+ "project_resource_page_limit",
33658
+ "conditional_inverse",
33659
+ "ambiguous_outcome_reconciliation"
33660
+ ],
33661
+ properties: {
33662
+ authority: { type: "string", enum: ["todos"] },
33663
+ route: { type: "string", enum: ["todos.project-registration.v1"] },
33664
+ package_version: { type: "string" },
33665
+ authority_id: { type: "string" },
33666
+ tenant_id: { type: "string" },
33667
+ corpus_id: { type: "string" },
33668
+ supported_resources: {
33669
+ type: "array",
33670
+ items: { type: "string", enum: ["project", "task_list"] }
33671
+ },
33672
+ conditional_create: { type: "boolean", enum: [true] },
33673
+ immutable_receipts: { type: "boolean", enum: [true] },
33674
+ exact_terminal_lookup: { type: "boolean", enum: [true] },
33675
+ exact_readback: { type: "boolean", enum: [true] },
33676
+ bind_existing_adoption: { type: "boolean", enum: [true] },
33677
+ prior_registration_adoption_validation: { type: "boolean", enum: [true] },
33678
+ project_resource_enumeration: { type: "boolean", enum: [true] },
33679
+ project_resource_page_limit: { type: "integer", minimum: 1 },
33680
+ conditional_inverse: { type: "boolean", enum: [true] },
33681
+ ambiguous_outcome_reconciliation: { type: "boolean", enum: [true] }
33682
+ }
33683
+ };
33684
+ projectRegistrationRequestSchema = {
33685
+ type: "object",
33686
+ additionalProperties: false,
33687
+ required: [
33688
+ "operation_id",
33689
+ "step_id",
33690
+ "resource_kind",
33691
+ "direction",
33692
+ "authority_route",
33693
+ "package_version",
33694
+ "authority_id",
33695
+ "tenant_id",
33696
+ "corpus_id",
33697
+ "target_selector",
33698
+ "idempotency_key",
33699
+ "request_digest",
33700
+ "precondition_digest",
33701
+ "project_id",
33702
+ "project_slug",
33703
+ "project_name",
33704
+ "desired",
33705
+ "response_byte_limit",
33706
+ "time_budget_ms"
33707
+ ],
33708
+ properties: {
33709
+ operation_id: { type: "string" },
33710
+ step_id: { type: "string" },
33711
+ resource_kind: { type: "string", enum: ["project", "task_list"] },
33712
+ direction: { type: "string", enum: ["forward", "inverse"] },
33713
+ authority_route: { type: "string" },
33714
+ package_version: { type: "string" },
33715
+ authority_id: { type: "string" },
33716
+ tenant_id: { type: "string" },
33717
+ corpus_id: { type: "string" },
33718
+ target_selector: { type: "string" },
33719
+ idempotency_key: { type: "string" },
33720
+ request_digest: { type: "string" },
33721
+ precondition_digest: { type: "string" },
33722
+ project_id: { type: "string" },
33723
+ project_slug: { type: "string" },
33724
+ project_name: { type: "string" },
33725
+ desired: { type: "object", additionalProperties: true },
33726
+ bind_existing: { type: "boolean" },
33727
+ accepted_receipt: { $ref: "#/components/schemas/ProjectRegistrationReceipt" },
33728
+ ...projectRegistrationBoundsProperties
33729
+ }
33730
+ };
33731
+ projectRegistrationLookupRequestSchema = {
33732
+ type: "object",
33733
+ additionalProperties: false,
33734
+ required: [
33735
+ "operation_id",
33736
+ "step_id",
33737
+ "resource_kind",
33738
+ "direction",
33739
+ "authority",
33740
+ "authority_route",
33741
+ "package_version",
33742
+ "authority_id",
33743
+ "tenant_id",
33744
+ "corpus_id",
33745
+ "target_selector",
33746
+ "idempotency_key",
33747
+ "max_items",
33748
+ "response_byte_limit",
33749
+ "time_budget_ms"
33750
+ ],
33751
+ properties: {
33752
+ operation_id: { type: "string" },
33753
+ step_id: { type: "string" },
33754
+ resource_kind: { type: "string", enum: ["project", "task_list"] },
33755
+ direction: { type: "string", enum: ["forward", "inverse"] },
33756
+ authority: { type: "string", enum: ["todos"] },
33757
+ authority_route: { type: "string" },
33758
+ package_version: { type: "string" },
33759
+ authority_id: { type: "string" },
33760
+ tenant_id: { type: "string" },
33761
+ corpus_id: { type: "string" },
33762
+ target_selector: { type: "string" },
33763
+ idempotency_key: { type: "string" },
33764
+ target_id: { type: "string", format: "uuid" },
33765
+ max_items: { type: "integer", enum: [1] },
33766
+ ...projectRegistrationBoundsProperties
33767
+ }
33768
+ };
33769
+ priorRegistrationAdoptionValidationSchema = {
33770
+ type: "object",
33771
+ additionalProperties: false,
33772
+ required: [
33773
+ "valid",
33774
+ "resource_kind",
33775
+ "target_id",
33776
+ "source_receipt_id",
33777
+ "accepted_receipt_id",
33778
+ "source_outcome",
33779
+ "created_at",
33780
+ "current_revision",
33781
+ "accepted_result_digest"
33782
+ ],
33783
+ properties: {
33784
+ valid: { type: "boolean", enum: [true] },
33785
+ resource_kind: { type: "string", enum: ["project", "task_list"] },
33786
+ target_id: { type: "string", format: "uuid" },
33787
+ source_receipt_id: { type: "string" },
33788
+ accepted_receipt_id: { type: "string" },
33789
+ source_outcome: { type: "string", enum: ["accepted", "duplicate_of_accepted"] },
33790
+ created_at: { type: "string", format: "date-time" },
33791
+ current_revision: { type: "string", format: "date-time" },
33792
+ accepted_result_digest: { type: "string" }
33793
+ }
33794
+ };
33795
+ projectResourceSchema = {
33796
+ type: "object",
33797
+ additionalProperties: false,
33798
+ required: [
33799
+ "source_project_id",
33800
+ "kind",
33801
+ "scope",
33802
+ "target_id",
33803
+ "parent_id",
33804
+ "revision",
33805
+ "digest"
33806
+ ],
33807
+ properties: {
33808
+ source_project_id: { type: "string" },
33809
+ kind: { type: "string", enum: ["project", "task_list", "plan", "task"] },
33810
+ scope: { type: "string", enum: ["collection", "resource"] },
33811
+ target_id: { type: "string", format: "uuid" },
33812
+ parent_id: { type: "string", format: "uuid", nullable: true },
33813
+ revision: { type: "string" },
33814
+ digest: { type: "string" }
33815
+ }
33816
+ };
33817
+ projectResourcePageSchema = {
33818
+ type: "object",
33819
+ additionalProperties: false,
33820
+ required: [
33821
+ "authority",
33822
+ "route",
33823
+ "package_version",
33824
+ "authority_id",
33825
+ "tenant_id",
33826
+ "corpus_id",
33827
+ "source_project_id",
33828
+ "todos_project_id",
33829
+ "task_list_id",
33830
+ "include_anchors",
33831
+ "collection_revision",
33832
+ "limit",
33833
+ "count",
33834
+ "resources",
33835
+ "has_more",
33836
+ "next_cursor",
33837
+ "complete",
33838
+ "truncated"
33839
+ ],
33840
+ properties: {
33841
+ authority: { type: "string", enum: ["todos"] },
33842
+ route: { type: "string", enum: ["todos.project-registration.v1"] },
33843
+ package_version: { type: "string" },
33844
+ authority_id: { type: "string" },
33845
+ tenant_id: { type: "string" },
33846
+ corpus_id: { type: "string" },
33847
+ source_project_id: { type: "string" },
33848
+ todos_project_id: { type: "string", format: "uuid" },
33849
+ task_list_id: { type: "string", format: "uuid" },
33850
+ include_anchors: { type: "boolean" },
33851
+ collection_revision: { type: "string" },
33852
+ limit: { type: "integer", minimum: 1, maximum: 500 },
33853
+ count: { type: "integer", minimum: 0 },
33854
+ resources: {
33855
+ type: "array",
33856
+ items: { $ref: "#/components/schemas/ProjectResource" }
33857
+ },
33858
+ has_more: { type: "boolean" },
33859
+ next_cursor: { type: "string", nullable: true },
33860
+ complete: { type: "boolean" },
33861
+ truncated: { type: "boolean", enum: [false] }
33862
+ }
33863
+ };
31997
33864
  });
31998
33865
 
31999
33866
  // src/server/pr-groups.ts
@@ -32140,11 +34007,11 @@ function canonicalJson2(value) {
32140
34007
  return `[${value.map(canonicalJson2).join(",")}]`;
32141
34008
  return `{${Object.entries(value).filter(([, item]) => item !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key2, item]) => `${JSON.stringify(key2)}:${canonicalJson2(item)}`).join(",")}}`;
32142
34009
  }
32143
- function digest(value) {
34010
+ function digest2(value) {
32144
34011
  return createHash9("sha256").update(canonicalJson2(value)).digest("hex");
32145
34012
  }
32146
34013
  function deriveIdempotencyKey(projectId, slug) {
32147
- return `ptlk_${digest({ project_id: projectId, slug }).slice(0, 48)}`;
34014
+ return `ptlk_${digest2({ project_id: projectId, slug }).slice(0, 48)}`;
32148
34015
  }
32149
34016
  function normalizeIdempotencyKey(value, projectId, slug) {
32150
34017
  const key2 = value?.trim() || deriveIdempotencyKey(projectId, slug);
@@ -32153,13 +34020,13 @@ function normalizeIdempotencyKey(value, projectId, slug) {
32153
34020
  }
32154
34021
  return key2;
32155
34022
  }
32156
- function receiptId2(projectId, slug, idempotencyKey) {
32157
- return `ptlr_${digest({ project_id: projectId, slug, idempotency_key: idempotencyKey }).slice(0, 48)}`;
34023
+ function receiptId2(projectId, slug, idempotencyKey2) {
34024
+ return `ptlr_${digest2({ project_id: projectId, slug, idempotency_key: idempotencyKey2 }).slice(0, 48)}`;
32158
34025
  }
32159
34026
  function semanticListDigest(list) {
32160
34027
  const metadata = { ...list.metadata ?? {} };
32161
34028
  delete metadata[RECEIPT_METADATA_KEY];
32162
- return digest({
34029
+ return digest2({
32163
34030
  project_id: list.project_id,
32164
34031
  slug: list.slug,
32165
34032
  name: list.name,
@@ -32176,10 +34043,10 @@ function storedMarker(list) {
32176
34043
  return null;
32177
34044
  return marker;
32178
34045
  }
32179
- function receiptFor(store, project, list, idempotencyKey) {
34046
+ function receiptFor(store, project, list, idempotencyKey2) {
32180
34047
  const marker = storedMarker(list);
32181
34048
  const owned = marker?.project_id === project.id && marker.slug === list.slug;
32182
- if (owned && marker.idempotency_key !== idempotencyKey) {
34049
+ if (owned && marker.idempotency_key !== idempotencyKey2) {
32183
34050
  throw new ProjectTaskListEnsureError("PROJECT_TASK_LIST_IDEMPOTENCY_CONFLICT", "The operation-owned task list was created under a different idempotency key", {
32184
34051
  project_id: project.id,
32185
34052
  task_list_id: list.id,
@@ -32188,8 +34055,8 @@ function receiptFor(store, project, list, idempotencyKey) {
32188
34055
  }
32189
34056
  return {
32190
34057
  schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
32191
- receipt_id: owned ? marker.receipt_id : `ptlr_existing_${digest({ project_id: project.id, task_list_id: list.id }).slice(0, 39)}`,
32192
- idempotency_key: owned ? marker.idempotency_key : idempotencyKey,
34058
+ receipt_id: owned ? marker.receipt_id : `ptlr_existing_${digest2({ project_id: project.id, task_list_id: list.id }).slice(0, 39)}`,
34059
+ idempotency_key: owned ? marker.idempotency_key : idempotencyKey2,
32193
34060
  project_id: project.id,
32194
34061
  task_list_id: list.id,
32195
34062
  slug: list.slug,
@@ -32241,20 +34108,20 @@ async function applyProjectTaskListEnsure(store, projectId, options) {
32241
34108
  });
32242
34109
  }
32243
34110
  const slug = project.task_list_id;
32244
- const idempotencyKey = normalizeIdempotencyKey(options.idempotency_key, project.id, slug);
34111
+ const idempotencyKey2 = normalizeIdempotencyKey(options.idempotency_key, project.id, slug);
32245
34112
  if (state.scoped) {
32246
34113
  return {
32247
34114
  mode: "apply",
32248
34115
  action: "already_present",
32249
34116
  project,
32250
34117
  task_list: state.scoped,
32251
- receipt: receiptFor(store, project, state.scoped, idempotencyKey)
34118
+ receipt: receiptFor(store, project, state.scoped, idempotencyKey2)
32252
34119
  };
32253
34120
  }
32254
34121
  const marker = {
32255
34122
  schema_version: PROJECT_TASK_LIST_ENSURE_SCHEMA_VERSION,
32256
- receipt_id: receiptId2(project.id, slug, idempotencyKey),
32257
- idempotency_key: idempotencyKey,
34123
+ receipt_id: receiptId2(project.id, slug, idempotencyKey2),
34124
+ idempotency_key: idempotencyKey2,
32258
34125
  project_id: project.id,
32259
34126
  slug,
32260
34127
  result_digest: semanticListDigest({
@@ -32292,7 +34159,7 @@ async function applyProjectTaskListEnsure(store, projectId, options) {
32292
34159
  action: "already_present",
32293
34160
  project: raced.project,
32294
34161
  task_list: raced.scoped,
32295
- receipt: receiptFor(store, raced.project, raced.scoped, idempotencyKey)
34162
+ receipt: receiptFor(store, raced.project, raced.scoped, idempotencyKey2)
32296
34163
  };
32297
34164
  }
32298
34165
  const projectReadback = await store.projects.get(project.id);
@@ -32322,7 +34189,7 @@ async function applyProjectTaskListEnsure(store, projectId, options) {
32322
34189
  action: "created",
32323
34190
  project: projectReadback,
32324
34191
  task_list: readback,
32325
- receipt: receiptFor(store, projectReadback, readback, idempotencyKey)
34192
+ receipt: receiptFor(store, projectReadback, readback, idempotencyKey2)
32326
34193
  };
32327
34194
  }
32328
34195
  async function rollbackProjectTaskListEnsure(store, projectId, options) {
@@ -32367,7 +34234,7 @@ async function rollbackProjectTaskListEnsure(store, projectId, options) {
32367
34234
  project_id: project.id,
32368
34235
  task_list_id: list.id,
32369
34236
  accepted_receipt_id: options.receipt_id,
32370
- rollback_receipt_id: `ptlr_inverse_${digest({ accepted_receipt_id: options.receipt_id }).slice(0, 38)}`,
34237
+ rollback_receipt_id: `ptlr_inverse_${digest2({ accepted_receipt_id: options.receipt_id }).slice(0, 38)}`,
32371
34238
  removed_at: new Date().toISOString()
32372
34239
  };
32373
34240
  }
@@ -32566,6 +34433,9 @@ function validateTaskPatchVocabulary(value) {
32566
34433
  if (!parsed.ok)
32567
34434
  return { ok: false, message: parsed.message };
32568
34435
  }
34436
+ if (body2.parent_id !== undefined && body2.parent_id !== null && (typeof body2.parent_id !== "string" || !body2.parent_id.trim())) {
34437
+ return { ok: false, message: "parent_id must be a non-empty task id or null" };
34438
+ }
32569
34439
  return { ok: true, patch: body2 };
32570
34440
  }
32571
34441
  function validateProjectPatch(value) {
@@ -33050,8 +34920,8 @@ async function handleV1Request(req, url, dependencies = {}) {
33050
34920
  }
33051
34921
  const created = await store.tasks.create(body2, storageContext);
33052
34922
  const persisted = created?.id ? await store.tasks.get(created.id, storageContext) : null;
33053
- if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== (body2.parent_id ?? null)) {
33054
- return error(500, "TASK_CREATE_PERSISTENCE_UNVERIFIED: task create was acknowledged but authoritative readback did not return the same stored task id and parent_id", { code: "TASK_CREATE_PERSISTENCE_UNVERIFIED" });
34923
+ if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== (body2.parent_id ?? null) || (persisted.plan_id ?? null) !== (body2.plan_id ?? null)) {
34924
+ return error(500, "TASK_CREATE_PERSISTENCE_UNVERIFIED: task create was acknowledged but authoritative readback did not return the same stored task id, parent_id, and plan_id", { code: "TASK_CREATE_PERSISTENCE_UNVERIFIED" });
33055
34925
  }
33056
34926
  return json5({ task: persisted }, 201);
33057
34927
  }
@@ -33925,6 +35795,15 @@ async function handleV1Request(req, url, dependencies = {}) {
33925
35795
  if (e instanceof TaskNotFoundError) {
33926
35796
  return error(404, e.message, { code: TaskNotFoundError.code });
33927
35797
  }
35798
+ if (e instanceof VersionConflictError) {
35799
+ return error(409, e.message, {
35800
+ code: VersionConflictError.code,
35801
+ conflict: true,
35802
+ task_id: e.taskId,
35803
+ expected_version: e.expectedVersion,
35804
+ current_version: e.actualVersion
35805
+ });
35806
+ }
33928
35807
  if (e instanceof StaleLockHandoffError) {
33929
35808
  const status2 = e.code === "STALE_LOCK_HANDOFF_INVALID_TASK_ID" || e.code === "STALE_LOCK_HANDOFF_INVALID_INPUT" ? 400 : e.code === "STALE_LOCK_HANDOFF_ACTOR_MISMATCH" ? 403 : 409;
33930
35809
  return error(status2, e.message, {
@@ -52540,16 +54419,16 @@ function createHasnaStorageClient(name, transport) {
52540
54419
  }
52541
54420
  },
52542
54421
  async create(resource, body2, options = {}) {
52543
- const { idempotencyKey, ...rest } = options;
54422
+ const { idempotencyKey: idempotencyKey2, ...rest } = options;
52544
54423
  return transport.post(resourcePath(resource), body2, {
52545
54424
  ...rest,
52546
- idempotencyKey: idempotencyKey ?? newIdempotencyKey()
54425
+ idempotencyKey: idempotencyKey2 ?? newIdempotencyKey()
52547
54426
  });
52548
54427
  },
52549
54428
  async update(resource, id, patch, options = {}) {
52550
- const { method = "PATCH", idempotencyKey, ...rest } = options;
54429
+ const { method = "PATCH", idempotencyKey: idempotencyKey2, ...rest } = options;
52551
54430
  const call = method === "PUT" ? transport.put : transport.patch;
52552
- return call(entityPath(resource, id), patch, { ...rest, ...idempotencyKey ? { idempotencyKey } : {} });
54431
+ return call(entityPath(resource, id), patch, { ...rest, ...idempotencyKey2 ? { idempotencyKey: idempotencyKey2 } : {} });
52553
54432
  },
52554
54433
  async delete(resource, id, options = {}) {
52555
54434
  try {
@@ -64733,6 +66612,11 @@ var init_http_client = __esm(() => {
64733
66612
  ]);
64734
66613
  });
64735
66614
 
66615
+ // src/project-registration/page-validation.ts
66616
+ var init_page_validation = __esm(() => {
66617
+ init_types3();
66618
+ });
66619
+
64736
66620
  // src/cli/cloud-router.ts
64737
66621
  import { randomUUID as randomUUID4 } from "crypto";
64738
66622
  import { resolve as resolvePath } from "path";
@@ -65131,12 +67015,39 @@ async function cloudListTasks(client, filter = {}) {
65131
67015
  union3.sort(compareCloudTaskOrder);
65132
67016
  return union3.slice(start, windowEnd);
65133
67017
  }
67018
+ async function cloudResolveTaskRef(client, ref) {
67019
+ const input = ref.trim().toLowerCase();
67020
+ if (!input)
67021
+ throw new Error("Task reference must not be empty");
67022
+ if (UUID_RE.test(input))
67023
+ return input;
67024
+ let task2;
67025
+ try {
67026
+ task2 = await cloudGetTask(client, input);
67027
+ } catch (error3) {
67028
+ const status2 = error3 && typeof error3 === "object" ? error3.status : undefined;
67029
+ if (status2 === 409) {
67030
+ const body2 = error3 && typeof error3 === "object" ? error3.body : undefined;
67031
+ const authorityMessage = body2 && typeof body2 === "object" && !Array.isArray(body2) ? body2.error : undefined;
67032
+ throw new Error(typeof authorityMessage === "string" && authorityMessage.trim() ? authorityMessage : `Task reference is ambiguous: "${ref}"`);
67033
+ }
67034
+ throw error3;
67035
+ }
67036
+ if (task2 && typeof task2.id === "string" && (task2.short_id?.toLowerCase() === input || task2.id.toLowerCase().startsWith(input))) {
67037
+ return task2.id;
67038
+ }
67039
+ if (task2) {
67040
+ throw new Error(`Task not found: ${ref} \u2014 the authority returned a task carrying neither this short id ` + "nor this id prefix, so the reference was not resolved.");
67041
+ }
67042
+ throw new Error(`Task not found: ${ref} \u2014 the authority resolved no task for this short reference. ` + "That is not proof the task is absent: an authority predating server-side " + "short-reference resolution answers EVERY short id and id prefix with the same 404. " + "Retry with the full task UUID to tell the two apart, or deploy the current " + "@hasna/todos /v1 server.");
67043
+ }
65134
67044
  async function cloudGetTask(client, id) {
65135
67045
  const raw = await client.get("tasks", id);
65136
67046
  return raw == null ? null : unwrapTask(raw);
65137
67047
  }
65138
67048
  async function cloudCreateTask(client, input, verification2 = {}) {
65139
67049
  const expectedParentId = typeof input["parent_id"] === "string" ? input["parent_id"] : null;
67050
+ const expectedPlanId = typeof input["plan_id"] === "string" ? input["plan_id"] : null;
65140
67051
  const expectedCreatedBy = typeof verification2.expectedCreatedBy === "string" && verification2.expectedCreatedBy.trim() ? verification2.expectedCreatedBy : null;
65141
67052
  if (expectedCreatedBy !== null) {
65142
67053
  await requireTaskCreatorCapability(client);
@@ -65149,9 +67060,9 @@ async function cloudCreateTask(client, input, verification2 = {}) {
65149
67060
  throw new Error(`REMOTE_API_INCOMPATIBLE: configured Todos authority ${remoteAuthorityBase(client)} returned a task create ` + "response without a stored task id; no success row or local SQLite fallback is permitted");
65150
67061
  }
65151
67062
  const persisted = await cloudGetTask(client, created.id);
65152
- if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== expectedParentId || expectedCreatedBy !== null && persisted.created_by !== expectedCreatedBy) {
67063
+ if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== expectedParentId || (persisted.plan_id ?? null) !== expectedPlanId || expectedCreatedBy !== null && persisted.created_by !== expectedCreatedBy) {
65153
67064
  const creatorDetail = expectedCreatedBy === null ? "" : ` and explicit created_by=${JSON.stringify(expectedCreatedBy)} ` + `(readback ${JSON.stringify(persisted?.created_by ?? null)})`;
65154
- throw new Error(`TASK_CREATE_PERSISTENCE_UNVERIFIED: configured Todos authority ${remoteAuthorityBase(client)} accepted ` + `POST /v1/tasks but authoritative GET /v1/tasks/${encodeURIComponent(created.id)} did not return the same ` + `stored task id and parent_id${creatorDetail}; no success row or local SQLite fallback is permitted`);
67065
+ throw new Error(`TASK_CREATE_PERSISTENCE_UNVERIFIED: configured Todos authority ${remoteAuthorityBase(client)} accepted ` + `POST /v1/tasks but authoritative GET /v1/tasks/${encodeURIComponent(created.id)} did not return the same ` + `stored task id, parent_id, and plan_id ` + `(requested plan_id=${JSON.stringify(expectedPlanId)}, readback=${JSON.stringify(persisted?.plan_id ?? null)})` + `${creatorDetail}; no success row or local SQLite fallback is permitted`);
65155
67066
  }
65156
67067
  return persisted;
65157
67068
  }
@@ -65420,6 +67331,8 @@ var init_cloud_router = __esm(() => {
65420
67331
  init_redaction();
65421
67332
  init_plan_project_link_contract();
65422
67333
  init_http_client();
67334
+ init_adoption_validation();
67335
+ init_page_validation();
65423
67336
  UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
65424
67337
  TRANSPORT_TOKENS = {
65425
67338
  sqlite: "sqlite",
@@ -65456,6 +67369,7 @@ function registerTaskCrudTools(server, ctx) {
65456
67369
  compact["version"] = task2.version;
65457
67370
  compact["created_at"] = task2.created_at;
65458
67371
  compact["task_list_id"] = task2.task_list_id;
67372
+ compact["parent_id"] = task2.parent_id;
65459
67373
  return compactJson(compact);
65460
67374
  }
65461
67375
  function versionFor(taskId, version2) {
@@ -65725,6 +67639,7 @@ ${task2.description}` : null
65725
67639
  priority: exports_external.enum(["low", "medium", "high", "critical"]).optional(),
65726
67640
  assigned_to: exports_external.string().nullable().optional().describe("Agent ID or name, null to unassign"),
65727
67641
  project_id: exports_external.string().nullable().optional(),
67642
+ parent_id: exports_external.string().nullable().optional().describe("Existing parent task ID/reference, null to detach"),
65728
67643
  task_list_id: exports_external.string().nullable().optional(),
65729
67644
  depends_on: exports_external.array(exports_external.string()).optional().describe("Full replacement array of dependency IDs"),
65730
67645
  tags: exports_external.array(exports_external.string()).optional(),
@@ -65756,6 +67671,9 @@ ${task2.description}` : null
65756
67671
  if (typeof patch.project_id === "string" && patch.project_id) {
65757
67672
  patch.project_id = await cloudResolveProjectRef(cloud, patch.project_id);
65758
67673
  }
67674
+ if (typeof patch.parent_id === "string" && patch.parent_id) {
67675
+ patch.parent_id = await cloudResolveTaskRef(cloud, patch.parent_id);
67676
+ }
65759
67677
  if (typeof patch.task_list_id === "string" && patch.task_list_id) {
65760
67678
  let scope = typeof patch.project_id === "string" ? patch.project_id : undefined;
65761
67679
  if (!scope) {
@@ -65764,9 +67682,22 @@ ${task2.description}` : null
65764
67682
  }
65765
67683
  patch.task_list_id = await cloudResolveTaskListRef(cloud, patch.task_list_id, scope);
65766
67684
  }
67685
+ if (patch.parent_id !== undefined && version3 === undefined) {
67686
+ const current = await cloudGetTask(cloud, task_id2);
67687
+ if (!current)
67688
+ throw new TaskNotFoundError(task_id2);
67689
+ patch.version = current.version;
67690
+ }
65767
67691
  if (version3 !== undefined)
65768
67692
  patch.version = version3;
65769
- const updated = await cloudUpdateTask(cloud, task_id2, patch);
67693
+ let updated = await cloudUpdateTask(cloud, task_id2, patch);
67694
+ if (patch.parent_id !== undefined) {
67695
+ const persisted = await cloudGetTask(cloud, task_id2);
67696
+ if (!persisted || (persisted.parent_id ?? null) !== patch.parent_id) {
67697
+ throw new Error(`TASK_REPARENT_PERSISTENCE_UNVERIFIED: parent_id expected ${patch.parent_id ?? "null"}, ` + `received ${persisted?.parent_id ?? "missing task"}`);
67698
+ }
67699
+ updated = persisted;
67700
+ }
65770
67701
  return { content: [{ type: "text", text: mutationTaskResponse(updated) }] };
65771
67702
  }
65772
67703
  const resolvedId = resolveId(params.task_id);
@@ -65778,6 +67709,8 @@ ${task2.description}` : null
65778
67709
  resolved.assigned_to = resolveAssignee(resolved.assigned_to);
65779
67710
  if (resolved.project_id && typeof resolved.project_id === "string")
65780
67711
  resolved.project_id = resolveId(resolved.project_id, "projects");
67712
+ if (resolved.parent_id && typeof resolved.parent_id === "string")
67713
+ resolved.parent_id = resolveId(resolved.parent_id);
65781
67714
  if (resolved.task_list_id && typeof resolved.task_list_id === "string")
65782
67715
  resolved.task_list_id = resolveId(resolved.task_list_id, "task_lists");
65783
67716
  if (resolved.depends_on && Array.isArray(resolved.depends_on))
@@ -99460,7 +101393,7 @@ function registerTaskMetaTools(server, ctx) {
99460
101393
  create_task: "create_task \u2014 Create a new task. Params: title (required), description, status, priority, project_id, task_list_id, assigned_to, depends_on, short_id (null to disable), tags, estimate (minutes), confidence (0.0-1.0), deadline (ISO), retry_count",
99461
101394
  list_tasks: "list_tasks \u2014 List tasks with filters. Params: status, priority, project_id, task_list_id, assigned_to, tags[], created_after, created_before, limit, offset",
99462
101395
  get_task: "get_task \u2014 Get compact task details by default. Params: task_id, detail=compact|full, max_description_chars, include_metadata",
99463
- update_task: "update_task \u2014 Update task fields (optimistic locking). Params: task_id (required), title, description, status, priority, assigned_to (null to unassign), project_id, task_list_id, depends_on[], tags[], estimate, actual_minutes, confidence, approved_by, completed_at, deadline, retry_count, version",
101396
+ update_task: "update_task \u2014 Update task fields (optimistic locking). Params: task_id (required), title, description, status, priority, assigned_to (null to unassign), project_id, parent_id (null to detach), task_list_id, depends_on[], tags[], estimate, actual_minutes, confidence, approved_by, completed_at, deadline, retry_count, version",
99464
101397
  delete_task: "delete_task \u2014 Delete a task. Params: task_id, force (skip child check)",
99465
101398
  start_task: "start_task \u2014 Mark task in_progress. Params: task_id, version",
99466
101399
  complete_task: "complete_task \u2014 Mark task completed. Params: task_id, confidence, completed_at, version",
@@ -111275,8 +113208,8 @@ function defaultSnapshotDir() {
111275
113208
  return join15(dirname8(resolve16(dbPath)), "environment-snapshots");
111276
113209
  }
111277
113210
  function snapshotWithId(snapshot) {
111278
- const digest2 = sha2567(JSON.stringify(snapshot)).slice(0, 24);
111279
- return { id: `env_${digest2}`, ...snapshot };
113211
+ const digest3 = sha2567(JSON.stringify(snapshot)).slice(0, 24);
113212
+ return { id: `env_${digest3}`, ...snapshot };
111280
113213
  }
111281
113214
  function captureEnvironmentSnapshot(input = {}) {
111282
113215
  const root = resolve16(input.root || process.cwd());