@hasna/todos 0.15.10 → 0.15.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -2123,7 +2123,7 @@ var package_default;
2123
2123
  var init_package = __esm(() => {
2124
2124
  package_default = {
2125
2125
  name: "@hasna/todos",
2126
- version: "0.15.10",
2126
+ version: "0.15.12",
2127
2127
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
2128
2128
  type: "module",
2129
2129
  main: "dist/index.js",
@@ -2269,7 +2269,7 @@ function isBlockingDependencyStatus(status) {
2269
2269
  function isTerminalStatus(status) {
2270
2270
  return status === "completed" || status === "failed" || status === "cancelled";
2271
2271
  }
2272
- var TASK_STATUSES, TASK_PRIORITIES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
2272
+ var TASK_STATUSES, TASK_PRIORITIES, PLAN_STATUSES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DISPATCH_STATUSES, DispatchNotFoundError;
2273
2273
  var init_types = __esm(() => {
2274
2274
  TASK_STATUSES = [
2275
2275
  "pending",
@@ -2284,6 +2284,7 @@ var init_types = __esm(() => {
2284
2284
  "high",
2285
2285
  "critical"
2286
2286
  ];
2287
+ PLAN_STATUSES = ["active", "completed", "archived"];
2287
2288
  VersionConflictError = class VersionConflictError extends Error {
2288
2289
  taskId;
2289
2290
  expectedVersion;
@@ -2822,6 +2823,405 @@ var init_redaction = __esm(() => {
2822
2823
  REDACTION_PLACEHOLDER = String.raw`\[REDACTED(?:_[A-Z_]+)?\]`;
2823
2824
  });
2824
2825
 
2826
+ // src/lib/plan-project-link-contract.ts
2827
+ import { createHash as createHash2 } from "crypto";
2828
+ function canonicalPlanProjectLinkJson(value) {
2829
+ if (value === null || typeof value !== "object")
2830
+ return JSON.stringify(value);
2831
+ if (Array.isArray(value))
2832
+ return `[${value.map(canonicalPlanProjectLinkJson).join(",")}]`;
2833
+ return `{${Object.entries(value).filter(([, item]) => item !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${canonicalPlanProjectLinkJson(item)}`).join(",")}}`;
2834
+ }
2835
+ function planProjectLinkDigest(value) {
2836
+ return createHash2("sha256").update(canonicalPlanProjectLinkJson(value)).digest("hex");
2837
+ }
2838
+ function normalizePlanProjectLinkIdempotencyKey(value) {
2839
+ const key = value?.trim() ?? "";
2840
+ if (key.length < 8 || key.length > 128 || !/^[A-Za-z0-9._:-]+$/.test(key)) {
2841
+ throw new PlanProjectLinkError("PLAN_PROJECT_LINK_IDEMPOTENCY_KEY_INVALID", "idempotency_key must be 8-128 ASCII letters, digits, dots, underscores, colons, or hyphens");
2842
+ }
2843
+ return key;
2844
+ }
2845
+ function planProjectLinkReceiptId(idempotencyKey) {
2846
+ return `pplr_${planProjectLinkDigest({ idempotency_key: idempotencyKey }).slice(0, 48)}`;
2847
+ }
2848
+ function planProjectLinkRollbackReceiptId(receiptId) {
2849
+ return `pplr_inverse_${planProjectLinkDigest({ accepted_receipt_id: receiptId }).slice(0, 38)}`;
2850
+ }
2851
+ function planProjectLinkRequestHash(planId, projectId) {
2852
+ return planProjectLinkDigest({ plan_id: planId, project_id: projectId });
2853
+ }
2854
+ function planProjectLinkResultDigest(plan, tasks) {
2855
+ return planProjectLinkDigest({
2856
+ plan_id: plan.id,
2857
+ plan_project_id: plan.project_id,
2858
+ tasks: tasks.map((task) => ({ id: task.id, plan_id: task.plan_id, project_id: task.project_id })).sort((left, right) => left.id.localeCompare(right.id))
2859
+ });
2860
+ }
2861
+ function assertPlanProjectLinkReceipt(value) {
2862
+ const receipt = value;
2863
+ if (!receipt || typeof receipt !== "object" || receipt.schema_version !== PLAN_PROJECT_LINK_SCHEMA_VERSION || typeof receipt.receipt_id !== "string" || typeof receipt.idempotency_key !== "string" || typeof receipt.plan_id !== "string" || typeof receipt.project_id !== "string" || !Array.isArray(receipt.task_ids) || receipt.task_ids.some((id) => typeof id !== "string") || !receipt.prior_task_project_ids || typeof receipt.prior_task_project_ids !== "object" || typeof receipt.result_digest !== "string") {
2864
+ throw new PlanProjectLinkError("PLAN_PROJECT_LINK_RECEIPT_NOT_FOUND", "Stored plan-project-link receipt is invalid");
2865
+ }
2866
+ return receipt;
2867
+ }
2868
+ function responseRecord(value, label) {
2869
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2870
+ throw new Error(`${label} must be an object`);
2871
+ }
2872
+ return value;
2873
+ }
2874
+ function exactResponseKeys(value, expected, label) {
2875
+ const actual = Object.keys(value).sort();
2876
+ const wanted = [...expected].sort();
2877
+ if (actual.length !== wanted.length || actual.some((key, index) => key !== wanted[index])) {
2878
+ throw new Error(`${label} fields must be exactly: ${wanted.join(", ")}`);
2879
+ }
2880
+ }
2881
+ function responseString(value, label) {
2882
+ if (typeof value !== "string" || value.length === 0) {
2883
+ throw new Error(`${label} must be a non-empty string`);
2884
+ }
2885
+ return value;
2886
+ }
2887
+ function responseNullableString(value, label) {
2888
+ if (value !== null && typeof value !== "string") {
2889
+ throw new Error(`${label} must be a string or null`);
2890
+ }
2891
+ return value;
2892
+ }
2893
+ function responseNumber(value, label) {
2894
+ if (typeof value !== "number" || !Number.isFinite(value)) {
2895
+ throw new Error(`${label} must be a finite number`);
2896
+ }
2897
+ return value;
2898
+ }
2899
+ function responseBoolean(value, label) {
2900
+ if (typeof value !== "boolean")
2901
+ throw new Error(`${label} must be a boolean`);
2902
+ return value;
2903
+ }
2904
+ function responseDateTime(value, label) {
2905
+ const timestamp = responseString(value, label);
2906
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/.test(timestamp) || !Number.isFinite(Date.parse(timestamp))) {
2907
+ throw new Error(`${label} must be an RFC 3339 date-time`);
2908
+ }
2909
+ return timestamp;
2910
+ }
2911
+ function responseNullableDateTime(value, label) {
2912
+ if (value === null)
2913
+ return null;
2914
+ return responseDateTime(value, label);
2915
+ }
2916
+ function responseOptionalNullableString(record, field, label) {
2917
+ if (field in record)
2918
+ responseNullableString(record[field], `${label}.${field}`);
2919
+ }
2920
+ function responseOptionalNullableDateTime(record, field, label) {
2921
+ if (field in record)
2922
+ responseNullableDateTime(record[field], `${label}.${field}`);
2923
+ }
2924
+ function responseStringArray(value, label) {
2925
+ if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
2926
+ throw new Error(`${label} must be an array of strings`);
2927
+ }
2928
+ return value;
2929
+ }
2930
+ function responseProjectSources(value, expectedProjectId) {
2931
+ if (!Array.isArray(value))
2932
+ throw new Error("project.sources must be an array");
2933
+ value.forEach((item, index) => {
2934
+ const label = `project.sources[${index}]`;
2935
+ const source = responseRecord(item, label);
2936
+ responseString(source.id, `${label}.id`);
2937
+ if (responseString(source.project_id, `${label}.project_id`) !== expectedProjectId) {
2938
+ throw new Error(`${label}.project_id must match project.id`);
2939
+ }
2940
+ responseString(source.type, `${label}.type`);
2941
+ responseString(source.name, `${label}.name`);
2942
+ responseString(source.uri, `${label}.uri`);
2943
+ responseNullableString(source.description, `${label}.description`);
2944
+ responseRecord(source.metadata, `${label}.metadata`);
2945
+ responseDateTime(source.created_at, `${label}.created_at`);
2946
+ responseDateTime(source.updated_at, `${label}.updated_at`);
2947
+ });
2948
+ }
2949
+ function responsePlan(value, expectedPlanId) {
2950
+ const plan = responseRecord(value, "plan");
2951
+ if (responseString(plan.id, "plan.id") !== expectedPlanId) {
2952
+ throw new Error(`plan.id must match requested plan ${expectedPlanId}`);
2953
+ }
2954
+ responseNullableString(plan.slug, "plan.slug");
2955
+ responseNullableString(plan.project_id, "plan.project_id");
2956
+ responseNullableString(plan.task_list_id, "plan.task_list_id");
2957
+ responseNullableString(plan.agent_id, "plan.agent_id");
2958
+ responseString(plan.name, "plan.name");
2959
+ responseNullableString(plan.description, "plan.description");
2960
+ if (typeof plan.status !== "string" || !PLAN_STATUSES.includes(plan.status)) {
2961
+ throw new Error(`plan.status must be one of: ${PLAN_STATUSES.join(", ")}`);
2962
+ }
2963
+ responseDateTime(plan.created_at, "plan.created_at");
2964
+ responseDateTime(plan.updated_at, "plan.updated_at");
2965
+ responseOptionalNullableString(plan, "machine_id", "plan");
2966
+ responseOptionalNullableDateTime(plan, "synced_at", "plan");
2967
+ return plan;
2968
+ }
2969
+ function responseProject(value, expectedProjectId) {
2970
+ const project = responseRecord(value, "project");
2971
+ if (responseString(project.id, "project.id") !== expectedProjectId) {
2972
+ throw new Error(`project.id must match requested project ${expectedProjectId}`);
2973
+ }
2974
+ responseString(project.name, "project.name");
2975
+ responseString(project.path, "project.path");
2976
+ responseNullableString(project.description, "project.description");
2977
+ responseNullableString(project.task_list_id, "project.task_list_id");
2978
+ responseNullableString(project.task_prefix, "project.task_prefix");
2979
+ responseNumber(project.task_counter, "project.task_counter");
2980
+ responseDateTime(project.created_at, "project.created_at");
2981
+ responseDateTime(project.updated_at, "project.updated_at");
2982
+ responseOptionalNullableString(project, "machine_id", "project");
2983
+ responseOptionalNullableDateTime(project, "synced_at", "project");
2984
+ if ("sources" in project && project.sources !== undefined) {
2985
+ responseProjectSources(project.sources, expectedProjectId);
2986
+ }
2987
+ return project;
2988
+ }
2989
+ function responseTasks(value, expectedPlanId) {
2990
+ if (!Array.isArray(value))
2991
+ throw new Error("tasks must be an array");
2992
+ const seen = new Set;
2993
+ return value.map((item, index) => {
2994
+ const task = responseRecord(item, `tasks[${index}]`);
2995
+ const taskId = responseString(task.id, `tasks[${index}].id`);
2996
+ if (seen.has(taskId))
2997
+ throw new Error(`tasks contains duplicate id ${taskId}`);
2998
+ seen.add(taskId);
2999
+ if (task.plan_id !== expectedPlanId) {
3000
+ throw new Error(`tasks[${index}].plan_id must match requested plan ${expectedPlanId}`);
3001
+ }
3002
+ const label = `tasks[${index}]`;
3003
+ responseNullableString(task.short_id, `${label}.short_id`);
3004
+ responseNullableString(task.project_id, `tasks[${index}].project_id`);
3005
+ responseNullableString(task.parent_id, `${label}.parent_id`);
3006
+ responseNullableString(task.task_list_id, `${label}.task_list_id`);
3007
+ responseString(task.title, `${label}.title`);
3008
+ responseNullableString(task.description, `${label}.description`);
3009
+ if (typeof task.status !== "string" || !TASK_STATUSES.includes(task.status)) {
3010
+ throw new Error(`${label}.status must be one of: ${TASK_STATUSES.join(", ")}`);
3011
+ }
3012
+ if (typeof task.priority !== "string" || !TASK_PRIORITIES.includes(task.priority)) {
3013
+ throw new Error(`${label}.priority must be one of: ${TASK_PRIORITIES.join(", ")}`);
3014
+ }
3015
+ for (const field of [
3016
+ "agent_id",
3017
+ "assigned_to",
3018
+ "session_id",
3019
+ "working_dir",
3020
+ "locked_by",
3021
+ "approved_by",
3022
+ "recurrence_rule",
3023
+ "recurrence_parent_id",
3024
+ "spawns_template_id",
3025
+ "reason",
3026
+ "spawned_from_session",
3027
+ "assigned_by",
3028
+ "created_by",
3029
+ "assigned_from_project",
3030
+ "task_type",
3031
+ "delegated_from",
3032
+ "runner_id",
3033
+ "current_step"
3034
+ ]) {
3035
+ responseNullableString(task[field], `${label}.${field}`);
3036
+ }
3037
+ responseStringArray(task.tags, `${label}.tags`);
3038
+ responseRecord(task.metadata, `${label}.metadata`);
3039
+ for (const field of [
3040
+ "version",
3041
+ "cost_tokens",
3042
+ "cost_usd",
3043
+ "delegation_depth",
3044
+ "retry_count",
3045
+ "max_retries"
3046
+ ]) {
3047
+ responseNumber(task[field], `${label}.${field}`);
3048
+ }
3049
+ for (const field of ["estimated_minutes", "actual_minutes", "confidence", "sla_minutes", "total_steps"]) {
3050
+ if (task[field] !== null)
3051
+ responseNumber(task[field], `${label}.${field}`);
3052
+ }
3053
+ responseBoolean(task.requires_approval, `${label}.requires_approval`);
3054
+ responseDateTime(task.created_at, `${label}.created_at`);
3055
+ responseDateTime(task.updated_at, `${label}.updated_at`);
3056
+ for (const field of [
3057
+ "locked_at",
3058
+ "started_at",
3059
+ "completed_at",
3060
+ "due_at",
3061
+ "approved_at",
3062
+ "retry_after",
3063
+ "runner_started_at",
3064
+ "runner_completed_at"
3065
+ ]) {
3066
+ responseNullableDateTime(task[field], `${label}.${field}`);
3067
+ }
3068
+ responseOptionalNullableString(task, "machine_id", label);
3069
+ responseOptionalNullableDateTime(task, "synced_at", label);
3070
+ responseOptionalNullableDateTime(task, "archived_at", label);
3071
+ return task;
3072
+ });
3073
+ }
3074
+ function responseReceipt(value, expectation, planRevision, taskIds) {
3075
+ const receipt = responseRecord(value, "receipt");
3076
+ exactResponseKeys(receipt, [
3077
+ "schema_version",
3078
+ "receipt_id",
3079
+ "idempotency_key",
3080
+ "plan_id",
3081
+ "project_id",
3082
+ "prior_plan_project_id",
3083
+ "prior_task_project_ids",
3084
+ "task_ids",
3085
+ "task_count",
3086
+ "result_plan_revision",
3087
+ "result_digest",
3088
+ "rollback_supported",
3089
+ "created_at"
3090
+ ], "receipt");
3091
+ if (receipt.schema_version !== PLAN_PROJECT_LINK_SCHEMA_VERSION) {
3092
+ throw new Error(`receipt.schema_version must be ${PLAN_PROJECT_LINK_SCHEMA_VERSION}`);
3093
+ }
3094
+ const expectedReceiptId = planProjectLinkReceiptId(expectation.idempotency_key);
3095
+ if (responseString(receipt.receipt_id, "receipt.receipt_id") !== expectedReceiptId) {
3096
+ throw new Error("receipt.receipt_id must match the deterministic apply receipt identity");
3097
+ }
3098
+ if (responseString(receipt.idempotency_key, "receipt.idempotency_key") !== expectation.idempotency_key) {
3099
+ throw new Error("receipt.idempotency_key must match the apply request");
3100
+ }
3101
+ if (responseString(receipt.plan_id, "receipt.plan_id") !== expectation.plan_id) {
3102
+ throw new Error("receipt.plan_id must match the requested plan");
3103
+ }
3104
+ if (responseString(receipt.project_id, "receipt.project_id") !== expectation.project_id) {
3105
+ throw new Error("receipt.project_id must match the requested project");
3106
+ }
3107
+ responseNullableString(receipt.prior_plan_project_id, "receipt.prior_plan_project_id");
3108
+ const priorTaskProjectIds = responseRecord(receipt.prior_task_project_ids, "receipt.prior_task_project_ids");
3109
+ for (const [taskId, projectId] of Object.entries(priorTaskProjectIds)) {
3110
+ responseString(taskId, "receipt.prior_task_project_ids key");
3111
+ responseNullableString(projectId, `receipt.prior_task_project_ids.${taskId}`);
3112
+ }
3113
+ if (!Array.isArray(receipt.task_ids) || receipt.task_ids.some((id) => typeof id !== "string" || id.length === 0)) {
3114
+ throw new Error("receipt.task_ids must be an array of non-empty strings");
3115
+ }
3116
+ const receiptTaskIds = receipt.task_ids;
3117
+ if (new Set(receiptTaskIds).size !== receiptTaskIds.length) {
3118
+ throw new Error("receipt.task_ids must not contain duplicates");
3119
+ }
3120
+ if (receiptTaskIds.length !== taskIds.length || receiptTaskIds.some((taskId, index) => taskId !== taskIds[index])) {
3121
+ throw new Error("receipt.task_ids must exactly match the response task identities");
3122
+ }
3123
+ const priorTaskIds = Object.keys(priorTaskProjectIds).sort();
3124
+ if (priorTaskIds.length !== taskIds.length || priorTaskIds.some((taskId, index) => taskId !== [...taskIds].sort()[index])) {
3125
+ throw new Error("receipt.prior_task_project_ids must exactly cover the response task identities");
3126
+ }
3127
+ if (!Number.isInteger(receipt.task_count) || receipt.task_count !== taskIds.length) {
3128
+ throw new Error("receipt.task_count must equal the response task count");
3129
+ }
3130
+ if (responseString(receipt.result_plan_revision, "receipt.result_plan_revision") !== planRevision) {
3131
+ throw new Error("receipt.result_plan_revision must equal plan.updated_at");
3132
+ }
3133
+ const resultDigest = responseString(receipt.result_digest, "receipt.result_digest");
3134
+ if (!/^[a-f0-9]{64}$/.test(resultDigest)) {
3135
+ throw new Error("receipt.result_digest must be a lowercase SHA-256 digest");
3136
+ }
3137
+ if (receipt.rollback_supported !== true) {
3138
+ throw new Error("receipt.rollback_supported must be true");
3139
+ }
3140
+ responseDateTime(receipt.created_at, "receipt.created_at");
3141
+ return receipt;
3142
+ }
3143
+ function assertPlanProjectLinkResponse(value, expectation) {
3144
+ const response = responseRecord(value, `${expectation.mode} response`);
3145
+ exactResponseKeys(response, ["mode", "action", "plan", "project", "tasks", "receipt"], `${expectation.mode} response`);
3146
+ if (response.mode !== expectation.mode) {
3147
+ throw new Error(`mode must be ${expectation.mode}`);
3148
+ }
3149
+ const allowedActions = expectation.mode === "plan" ? ["would_link", "already_linked"] : ["linked", "already_linked"];
3150
+ if (typeof response.action !== "string" || !allowedActions.includes(response.action)) {
3151
+ throw new Error(`action must be one of: ${allowedActions.join(", ")}`);
3152
+ }
3153
+ const plan = responsePlan(response.plan, expectation.plan_id);
3154
+ responseProject(response.project, expectation.project_id);
3155
+ const tasks = responseTasks(response.tasks, expectation.plan_id);
3156
+ const linkedResult = expectation.mode === "apply" || response.action === "already_linked";
3157
+ if (linkedResult) {
3158
+ if (plan.project_id !== expectation.project_id) {
3159
+ throw new Error("plan.project_id must match the requested project after linkage");
3160
+ }
3161
+ if (tasks.some((task) => task.project_id !== expectation.project_id)) {
3162
+ throw new Error("every task.project_id must match the requested project after linkage");
3163
+ }
3164
+ }
3165
+ if (expectation.mode === "plan") {
3166
+ if (response.receipt !== null)
3167
+ throw new Error("receipt must be null for a plan response");
3168
+ } else {
3169
+ if (!expectation.idempotency_key)
3170
+ throw new Error("apply validation requires the request idempotency key");
3171
+ const receipt = responseReceipt(response.receipt, expectation, plan.updated_at, tasks.map((task) => task.id));
3172
+ const expectedDigest = planProjectLinkResultDigest(plan, tasks);
3173
+ if (receipt.result_digest !== expectedDigest) {
3174
+ throw new Error("receipt.result_digest must match the returned plan and tasks");
3175
+ }
3176
+ }
3177
+ return value;
3178
+ }
3179
+ function assertPlanProjectLinkRollbackResponse(value, expectation) {
3180
+ const response = responseRecord(value, "rollback response");
3181
+ exactResponseKeys(response, [
3182
+ "schema_version",
3183
+ "action",
3184
+ "plan",
3185
+ "tasks",
3186
+ "accepted_receipt_id",
3187
+ "rollback_receipt_id",
3188
+ "restored_at"
3189
+ ], "rollback response");
3190
+ if (response.schema_version !== PLAN_PROJECT_LINK_SCHEMA_VERSION) {
3191
+ throw new Error(`schema_version must be ${PLAN_PROJECT_LINK_SCHEMA_VERSION}`);
3192
+ }
3193
+ if (response.action !== "restored")
3194
+ throw new Error("action must be restored");
3195
+ const plan = responsePlan(response.plan, expectation.plan_id);
3196
+ responseTasks(response.tasks, expectation.plan_id);
3197
+ if (responseString(response.accepted_receipt_id, "accepted_receipt_id") !== expectation.receipt_id) {
3198
+ throw new Error("accepted_receipt_id must match the rollback request");
3199
+ }
3200
+ const expectedRollbackReceiptId = planProjectLinkRollbackReceiptId(expectation.receipt_id);
3201
+ if (responseString(response.rollback_receipt_id, "rollback_receipt_id") !== expectedRollbackReceiptId) {
3202
+ throw new Error("rollback_receipt_id must match the deterministic rollback receipt identity");
3203
+ }
3204
+ const restoredAt = responseDateTime(response.restored_at, "restored_at");
3205
+ if (plan.updated_at !== restoredAt) {
3206
+ throw new Error("restored_at must equal plan.updated_at");
3207
+ }
3208
+ return value;
3209
+ }
3210
+ var PLAN_PROJECT_LINK_SCHEMA_VERSION = "todos.plan-project-link.v1", PlanProjectLinkError;
3211
+ var init_plan_project_link_contract = __esm(() => {
3212
+ init_types();
3213
+ PlanProjectLinkError = class PlanProjectLinkError extends Error {
3214
+ code;
3215
+ details;
3216
+ constructor(code, message, details = {}) {
3217
+ super(message);
3218
+ this.code = code;
3219
+ this.details = details;
3220
+ this.name = "PlanProjectLinkError";
3221
+ }
3222
+ };
3223
+ });
3224
+
2825
3225
  // src/pr-groups/types.ts
2826
3226
  var PR_GROUP_LEDGER_SCHEMA_VERSION = 1, PR_GROUP_REPAIR_CYCLE_LIMIT = 2, PrGroupLedgerError;
2827
3227
  var init_types2 = __esm(() => {
@@ -2838,9 +3238,9 @@ var init_types2 = __esm(() => {
2838
3238
  });
2839
3239
 
2840
3240
  // src/pr-groups/ledger.ts
2841
- import { createHash as createHash2 } from "crypto";
3241
+ import { createHash as createHash3 } from "crypto";
2842
3242
  function sha256(value) {
2843
- return createHash2("sha256").update(value).digest("hex");
3243
+ return createHash3("sha256").update(value).digest("hex");
2844
3244
  }
2845
3245
  function stableValue(value) {
2846
3246
  if (Array.isArray(value))
@@ -5376,6 +5776,12 @@ function toListQuery(filter = {}) {
5376
5776
  query["offset"] = filter.offset;
5377
5777
  return query;
5378
5778
  }
5779
+ function priorityRank(priority) {
5780
+ return PRIORITY_RANK[priority ?? ""] ?? 4;
5781
+ }
5782
+ function compareCloudTaskOrder(a, b) {
5783
+ return priorityRank(a.priority) - priorityRank(b.priority) || b.created_at.localeCompare(a.created_at) || a.id.localeCompare(b.id);
5784
+ }
5379
5785
  async function fetchListTagsCapability(client) {
5380
5786
  const document = await requiredRemoteRoute(client, "/v1/openapi.json", () => client.transport.get("/openapi.json"));
5381
5787
  if (!document || typeof document !== "object" || Array.isArray(document))
@@ -5397,13 +5803,40 @@ async function requireTagsFilterCapability(client) {
5397
5803
  throw new Error(`REMOTE_TAGS_FILTER_UNSUPPORTED: configured Todos authority ${authority} does not advertise the tags ` + "query param on GET /v1/tasks; deploy the current @hasna/todos /v1 server to filter by tag; " + "no unfiltered task read was issued");
5398
5804
  }
5399
5805
  }
5400
- async function cloudListTasks(client, filter = {}) {
5401
- if (filter.tags?.length)
5402
- await requireTagsFilterCapability(client);
5806
+ async function requestCloudTaskPage(client, filter) {
5403
5807
  const res = await requiredRemoteRoute(client, "/v1/tasks", () => client.list("tasks", { query: toListQuery(filter) }));
5404
5808
  const envelope = res.raw;
5405
5809
  return Array.isArray(envelope?.tasks) ? envelope.tasks : res.items;
5406
5810
  }
5811
+ async function cloudListTasks(client, filter = {}) {
5812
+ if (filter.tags?.length)
5813
+ await requireTagsFilterCapability(client);
5814
+ const statuses = Array.isArray(filter.status) ? filter.status : undefined;
5815
+ if (!statuses)
5816
+ return requestCloudTaskPage(client, filter);
5817
+ if (statuses.length === 0)
5818
+ return [];
5819
+ if (statuses.length === 1) {
5820
+ return requestCloudTaskPage(client, { ...filter, status: statuses[0] });
5821
+ }
5822
+ const { status: _status, limit, offset, ...baseFilter } = filter;
5823
+ const start = offset ?? 0;
5824
+ const windowEnd = typeof limit === "number" ? start + limit : undefined;
5825
+ const pages = await Promise.all(statuses.map((status) => requestCloudTaskPage(client, {
5826
+ ...baseFilter,
5827
+ status,
5828
+ ...windowEnd === undefined ? {} : { limit: windowEnd }
5829
+ })));
5830
+ const seen = new Set;
5831
+ const union = pages.flat().filter((task) => {
5832
+ if (seen.has(task.id))
5833
+ return false;
5834
+ seen.add(task.id);
5835
+ return true;
5836
+ });
5837
+ union.sort(compareCloudTaskOrder);
5838
+ return union.slice(start, windowEnd);
5839
+ }
5407
5840
  async function cloudResolveTaskRef(client, ref) {
5408
5841
  const input = ref.trim().toLowerCase();
5409
5842
  if (!input)
@@ -5711,17 +6144,50 @@ async function cloudRollbackProjectTaskListEnsure(client, projectId, input) {
5711
6144
  return requiredRemoteRoute(client, "/v1/projects/:id/task-list/rollback", () => client.transport.post(`/projects/${encodeURIComponent(projectId)}/task-list/rollback`, input), ["PROJECT_NOT_FOUND", "PROJECT_TASK_LIST_RECEIPT_NOT_FOUND"]);
5712
6145
  }
5713
6146
  async function cloudPlanPlanProjectLink(client, planId, projectId) {
5714
- return requiredRemoteRoute(client, "/v1/plans/:id/project-link", () => client.transport.get(`/plans/${encodeURIComponent(planId)}/project-link`, { query: { project_id: projectId } }), ["PLAN_PROJECT_LINK_PLAN_NOT_FOUND", "PLAN_PROJECT_LINK_PROJECT_NOT_FOUND"]);
6147
+ const route = `/v1/plans/${encodeURIComponent(planId)}/project-link`;
6148
+ const response = await requiredRemoteRoute(client, "/v1/plans/:id/project-link", () => client.transport.get(`/plans/${encodeURIComponent(planId)}/project-link`, { query: { project_id: projectId } }), ["PLAN_PROJECT_LINK_PLAN_NOT_FOUND", "PLAN_PROJECT_LINK_PROJECT_NOT_FOUND"]);
6149
+ try {
6150
+ return assertPlanProjectLinkResponse(response, { mode: "plan", plan_id: planId, project_id: projectId });
6151
+ } catch (error) {
6152
+ const reason = error instanceof Error ? error.message : String(error);
6153
+ throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} returned an invalid ${PLAN_PROJECT_LINK_SCHEMA_VERSION} plan response: ` + `${reason}; local SQLite fallback is disabled`, { cause: error });
6154
+ }
5715
6155
  }
5716
6156
  async function cloudApplyPlanProjectLink(client, planId, projectId, input) {
5717
- return requiredRemoteRoute(client, "/v1/plans/:id/project-link", () => client.transport.post(`/plans/${encodeURIComponent(planId)}/project-link`, { project_id: projectId, ...input }), ["PLAN_PROJECT_LINK_PLAN_NOT_FOUND", "PLAN_PROJECT_LINK_PROJECT_NOT_FOUND"]);
6157
+ const route = `/v1/plans/${encodeURIComponent(planId)}/project-link`;
6158
+ const normalizedInput = {
6159
+ ...input,
6160
+ idempotency_key: normalizePlanProjectLinkIdempotencyKey(input.idempotency_key)
6161
+ };
6162
+ const response = await requiredRemoteRoute(client, "/v1/plans/:id/project-link", () => client.transport.post(`/plans/${encodeURIComponent(planId)}/project-link`, { project_id: projectId, ...normalizedInput }), ["PLAN_PROJECT_LINK_PLAN_NOT_FOUND", "PLAN_PROJECT_LINK_PROJECT_NOT_FOUND"]);
6163
+ try {
6164
+ return assertPlanProjectLinkResponse(response, {
6165
+ mode: "apply",
6166
+ plan_id: planId,
6167
+ project_id: projectId,
6168
+ idempotency_key: normalizedInput.idempotency_key
6169
+ });
6170
+ } catch (error) {
6171
+ const reason = error instanceof Error ? error.message : String(error);
6172
+ throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} returned an invalid ${PLAN_PROJECT_LINK_SCHEMA_VERSION} apply response: ` + `${reason}; local SQLite fallback is disabled`, { cause: error });
6173
+ }
5718
6174
  }
5719
6175
  async function cloudRollbackPlanProjectLink(client, planId, projectId, input) {
5720
- return requiredRemoteRoute(client, "/v1/plans/:id/project-link/rollback", () => client.transport.post(`/plans/${encodeURIComponent(planId)}/project-link/rollback`, { project_id: projectId, ...input }), [
6176
+ const route = `/v1/plans/${encodeURIComponent(planId)}/project-link/rollback`;
6177
+ const response = await requiredRemoteRoute(client, "/v1/plans/:id/project-link/rollback", () => client.transport.post(`/plans/${encodeURIComponent(planId)}/project-link/rollback`, { project_id: projectId, ...input }), [
5721
6178
  "PLAN_PROJECT_LINK_PLAN_NOT_FOUND",
5722
6179
  "PLAN_PROJECT_LINK_PROJECT_NOT_FOUND",
5723
6180
  "PLAN_PROJECT_LINK_RECEIPT_NOT_FOUND"
5724
6181
  ]);
6182
+ try {
6183
+ return assertPlanProjectLinkRollbackResponse(response, {
6184
+ plan_id: planId,
6185
+ receipt_id: input.receipt_id
6186
+ });
6187
+ } catch (error) {
6188
+ const reason = error instanceof Error ? error.message : String(error);
6189
+ throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} returned an invalid ${PLAN_PROJECT_LINK_SCHEMA_VERSION} rollback response: ` + `${reason}; local SQLite fallback is disabled`, { cause: error });
6190
+ }
5725
6191
  }
5726
6192
  async function cloudListPlans(client, projectId) {
5727
6193
  const query = projectId ? { project_id: projectId } : {};
@@ -5908,17 +6374,105 @@ async function cloudFindCommit(client, sha) {
5908
6374
  const env = raw ?? {};
5909
6375
  return env.commit ?? null;
5910
6376
  }
6377
+ function parseCloudTaskGitRef(value, route) {
6378
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
6379
+ throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} returned a non-object git ref; local SQLite fallback is disabled`);
6380
+ }
6381
+ const ref = value;
6382
+ const requiredStrings = ["id", "task_id", "name", "created_at", "updated_at"];
6383
+ if (requiredStrings.some((field) => typeof ref[field] !== "string" || !ref[field].trim())) {
6384
+ throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} returned an incomplete git ref identity; local SQLite fallback is disabled`);
6385
+ }
6386
+ if (ref["ref_type"] !== "branch" && ref["ref_type"] !== "pull_request") {
6387
+ throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} returned an invalid git ref type; local SQLite fallback is disabled`);
6388
+ }
6389
+ for (const field of ["url", "provider"]) {
6390
+ if (ref[field] !== null && typeof ref[field] !== "string") {
6391
+ throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} returned an invalid git ref ${field}; local SQLite fallback is disabled`);
6392
+ }
6393
+ }
6394
+ if (!ref["metadata"] || typeof ref["metadata"] !== "object" || Array.isArray(ref["metadata"])) {
6395
+ throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} returned invalid git ref metadata; local SQLite fallback is disabled`);
6396
+ }
6397
+ return ref;
6398
+ }
6399
+ function parseCloudTaskGitRefEnvelope(raw, route) {
6400
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
6401
+ throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} returned a non-object git ref envelope; local SQLite fallback is disabled`);
6402
+ }
6403
+ const envelope = raw;
6404
+ if (!Array.isArray(envelope["refs"]) || !Number.isSafeInteger(envelope["count"]) || envelope["count"] !== envelope["refs"].length) {
6405
+ throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} returned an incomplete git ref envelope; local SQLite fallback is disabled`);
6406
+ }
6407
+ return envelope["refs"].map((ref) => parseCloudTaskGitRef(ref, route));
6408
+ }
6409
+ function openApiHasOperation(document, path, method) {
6410
+ if (!document || typeof document !== "object" || Array.isArray(document))
6411
+ return false;
6412
+ const paths = document["paths"];
6413
+ if (!paths || typeof paths !== "object" || Array.isArray(paths))
6414
+ return false;
6415
+ const route = paths[path];
6416
+ return Boolean(route && typeof route === "object" && !Array.isArray(route) && route[method] && typeof route[method] === "object");
6417
+ }
6418
+ async function fetchGitRefCapabilities(client) {
6419
+ const document = await requiredRemoteRoute(client, "/v1/openapi.json", () => client.transport.get("/openapi.json"));
6420
+ const supported = new Set;
6421
+ if (openApiHasOperation(document, "/v1/tasks/{id}/refs", "get"))
6422
+ supported.add("task-read");
6423
+ if (openApiHasOperation(document, "/v1/tasks/{id}/refs", "post"))
6424
+ supported.add("task-write");
6425
+ if (openApiHasOperation(document, "/v1/refs/{ref}", "get"))
6426
+ supported.add("reverse-read");
6427
+ return supported;
6428
+ }
6429
+ async function requireGitRefCapabilities(client, required) {
6430
+ const authority = remoteAuthorityBase(client);
6431
+ let capabilities = gitRefCapabilityCache.get(authority);
6432
+ if (!capabilities) {
6433
+ capabilities = fetchGitRefCapabilities(client);
6434
+ gitRefCapabilityCache.set(authority, capabilities);
6435
+ }
6436
+ const supported = await capabilities;
6437
+ const missing = required.filter((capability) => !supported.has(capability));
6438
+ if (missing.length > 0) {
6439
+ throw new Error(`REMOTE_GIT_REF_UNSUPPORTED: configured Todos authority ${authority} does not advertise the complete git-ref ` + `contract (${missing.join(", ")} missing); deploy the current @hasna/todos /v1 server before retrying; ` + "no ref mutation or local SQLite fallback was attempted");
6440
+ }
6441
+ }
6442
+ function sameCloudTaskGitRef(ref, expected) {
6443
+ return ref.id === expected.id && ref.task_id === expected.task_id && ref.ref_type === expected.ref_type && ref.name === expected.name;
6444
+ }
6445
+ async function cloudListTaskRefs(client, taskId) {
6446
+ await requireGitRefCapabilities(client, ["task-read"]);
6447
+ const route = `/v1/tasks/${encodeURIComponent(taskId)}/refs`;
6448
+ const raw = await requiredRemoteRoute(client, route, () => client.transport.get(`/tasks/${encodeURIComponent(taskId)}/refs`));
6449
+ return parseCloudTaskGitRefEnvelope(raw, route);
6450
+ }
5911
6451
  async function cloudLinkRef(client, taskId, input) {
5912
- const raw = await client.transport.post(`/tasks/${encodeURIComponent(taskId)}/refs`, input);
5913
- if (raw && typeof raw === "object" && "ref" in raw) {
5914
- return raw.ref;
6452
+ await requireGitRefCapabilities(client, ["task-read", "task-write", "reverse-read"]);
6453
+ const route = `/v1/tasks/${encodeURIComponent(taskId)}/refs`;
6454
+ const raw = await requiredRemoteRoute(client, route, () => client.transport.post(`/tasks/${encodeURIComponent(taskId)}/refs`, input));
6455
+ if (!raw || typeof raw !== "object" || Array.isArray(raw) || Object.keys(raw).length !== 1 || !("ref" in raw)) {
6456
+ throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} returned a non-authoritative git ref response envelope; ` + "local SQLite fallback is disabled");
6457
+ }
6458
+ const linked = parseCloudTaskGitRef(raw.ref, route);
6459
+ if (linked.task_id !== taskId || linked.ref_type !== input.ref_type || linked.name !== input.name) {
6460
+ throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} returned a different git ref identity; local SQLite fallback is disabled`);
6461
+ }
6462
+ const [taskRefs, reverseRefs] = await Promise.all([
6463
+ cloudListTaskRefs(client, taskId),
6464
+ cloudFindRefs(client, input.name)
6465
+ ]);
6466
+ if (!taskRefs.some((ref) => sameCloudTaskGitRef(ref, linked)) || !reverseRefs.some((ref) => sameCloudTaskGitRef(ref, linked))) {
6467
+ throw new Error(`REMOTE_REF_PERSISTENCE_UNVERIFIED: configured Todos authority ${remoteAuthorityBase(client)} accepted ${route} ` + "but authoritative task and reverse readback did not return the linked ref; no success line or local SQLite " + "fallback is permitted");
5915
6468
  }
5916
- return raw;
6469
+ return linked;
5917
6470
  }
5918
6471
  async function cloudFindRefs(client, ref) {
5919
- const raw = await client.transport.get(`/refs/${encodeURIComponent(ref)}`);
5920
- const env = raw ?? {};
5921
- return Array.isArray(env.refs) ? env.refs : [];
6472
+ await requireGitRefCapabilities(client, ["reverse-read"]);
6473
+ const route = `/v1/refs/${encodeURIComponent(ref)}`;
6474
+ const raw = await requiredRemoteRoute(client, route, () => client.transport.get(`/refs/${encodeURIComponent(ref)}`));
6475
+ return parseCloudTaskGitRefEnvelope(raw, route);
5922
6476
  }
5923
6477
  async function cloudResolvePlan(client, ref, projectId) {
5924
6478
  const normalizedRef = ref.toLowerCase();
@@ -6076,9 +6630,6 @@ async function cloudRecordVerification(client, id, input) {
6076
6630
  }
6077
6631
  return raw;
6078
6632
  }
6079
- function priorityRank(priority) {
6080
- return PRIORITY_RANK[priority ?? ""] ?? 4;
6081
- }
6082
6633
  async function cloudActiveTasks(client, filter = {}) {
6083
6634
  const [pending, inProgress] = await Promise.all([
6084
6635
  cloudListTasks(client, { ...filter, status: "pending" }),
@@ -6392,10 +6943,11 @@ async function cloudTimeline(client, options = {}) {
6392
6943
  const limit = options.limit ?? 50;
6393
6944
  return { entries: entries.slice(offset, offset + limit), total, limit, offset };
6394
6945
  }
6395
- var UUID_RE, TRANSPORT_TOKENS, COMPLETION_EVIDENCE_FIELDS, completionCapabilityCache, SERVER_MODE_CANDIDATES, cachedServerMode = null, listTagsCapabilityCache, RELATION_HYDRATION_CONCURRENCY = 6, PRIORITY_RANK;
6946
+ var UUID_RE, TRANSPORT_TOKENS, COMPLETION_EVIDENCE_FIELDS, completionCapabilityCache, gitRefCapabilityCache, SERVER_MODE_CANDIDATES, cachedServerMode = null, PRIORITY_RANK, listTagsCapabilityCache, RELATION_HYDRATION_CONCURRENCY = 6;
6396
6947
  var init_cloud_router = __esm(() => {
6397
6948
  init_types();
6398
6949
  init_redaction();
6950
+ init_plan_project_link_contract();
6399
6951
  init_http_client();
6400
6952
  UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
6401
6953
  TRANSPORT_TOKENS = {
@@ -6416,9 +6968,10 @@ var init_cloud_router = __esm(() => {
6416
6968
  "confidence"
6417
6969
  ];
6418
6970
  completionCapabilityCache = new Map;
6971
+ gitRefCapabilityCache = new Map;
6419
6972
  SERVER_MODE_CANDIDATES = ["postgres", "cloud", "self_hosted"];
6420
- listTagsCapabilityCache = new Map;
6421
6973
  PRIORITY_RANK = { critical: 0, high: 1, medium: 2, low: 3 };
6974
+ listTagsCapabilityCache = new Map;
6422
6975
  });
6423
6976
 
6424
6977
  // src/cli/stage-a.ts
@@ -12815,7 +13368,7 @@ __export(exports_event_hooks, {
12815
13368
  emitLocalEventHooks: () => emitLocalEventHooks,
12816
13369
  LOCAL_EVENT_TYPES: () => LOCAL_EVENT_TYPES
12817
13370
  });
12818
- import { createHash as createHash3, randomUUID } from "crypto";
13371
+ import { createHash as createHash4, randomUUID } from "crypto";
12819
13372
  import { appendFileSync, mkdirSync as mkdirSync4 } from "fs";
12820
13373
  import { dirname as dirname4, resolve as resolve7 } from "path";
12821
13374
  import { createConnection } from "net";
@@ -12882,7 +13435,7 @@ function buildEnvelope(type, payload, timestamp2 = new Date().toISOString()) {
12882
13435
  payload: redactValue(payload ?? {}),
12883
13436
  source: { package: "@hasna/todos", local_only: true }
12884
13437
  };
12885
- const digest = createHash3("sha256").update(canonicalEvent(base)).digest("hex");
13438
+ const digest = createHash4("sha256").update(canonicalEvent(base)).digest("hex");
12886
13439
  return { ...base, integrity: { algorithm: "sha256", digest } };
12887
13440
  }
12888
13441
  function summarize(value) {
@@ -18096,7 +18649,7 @@ var init_boards = __esm(() => {
18096
18649
  });
18097
18650
 
18098
18651
  // src/lib/artifact-store.ts
18099
- import { createHash as createHash4 } from "crypto";
18652
+ import { createHash as createHash5 } from "crypto";
18100
18653
  import { existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync3, rmSync as rmSync2, statSync as statSync2, writeFileSync as writeFileSync3 } from "fs";
18101
18654
  import { basename as basename2, dirname as dirname5, join as join7, resolve as resolve8 } from "path";
18102
18655
  import { tmpdir as tmpdir3 } from "os";
@@ -18121,7 +18674,7 @@ function artifactStorePath(relativePath) {
18121
18674
  return join7(artifactStoreRoot(), normalized);
18122
18675
  }
18123
18676
  function sha2562(buffer) {
18124
- return createHash4("sha256").update(buffer).digest("hex");
18677
+ return createHash5("sha256").update(buffer).digest("hex");
18125
18678
  }
18126
18679
  function isTextLike(buffer, path) {
18127
18680
  if (buffer.includes(0))
@@ -20953,6 +21506,14 @@ async function cloudDetailRelations(cloud, id) {
20953
21506
  return { dependencies: [], blocked_by: [], blocks: [] };
20954
21507
  }
20955
21508
  }
21509
+ async function cloudDetailGitRefs(cloud, id) {
21510
+ try {
21511
+ return await cloudListTaskRefs(cloud, id);
21512
+ } catch (e) {
21513
+ console.error(chalk3.dim(`Warning: could not verify task git refs: ${e instanceof Error ? e.message : String(e)}`));
21514
+ return null;
21515
+ }
21516
+ }
20956
21517
  function resolveProjectIdOrSlug(input) {
20957
21518
  const db = getDatabase();
20958
21519
  if (isPathLike(input)) {
@@ -21525,7 +22086,8 @@ function registerTaskCommands(program2) {
21525
22086
  }
21526
22087
  const creatorFilterActive = Boolean(filter["created_by"] || filter["not_created_by"]);
21527
22088
  const requestedLimit = filter["limit"];
21528
- const reordersAfterQuery = Boolean(opts.sort);
22089
+ const combinesScalarStatusPages = Boolean(cloud && Array.isArray(filter["status"]) && filter["status"].length > 1);
22090
+ const reordersAfterQuery = Boolean(opts.sort) || combinesScalarStatusPages;
21529
22091
  const narrowsAfterQuery = Boolean(opts.dueToday) || Boolean(opts.overdue) || creatorFilterActive && cloud;
21530
22092
  const withholdLimit = requestedLimit !== undefined && (reordersAfterQuery || narrowsAfterQuery);
21531
22093
  const scanCeiling = cloud && (withholdLimit || requestedLimit === undefined) ? Math.max(requestedLimit ?? 0, listScanLimit()) : undefined;
@@ -21676,8 +22238,11 @@ function registerTaskCommands(program2) {
21676
22238
  let task2;
21677
22239
  if (cloud) {
21678
22240
  const remote = await cloudGetTask(cloud, await resolveTaskIdForCommand(id, cloud));
21679
- const commentPage = remote ? await cloudListComments(cloud, remote.id, page.request) : null;
21680
- const relations = remote ? await cloudDetailRelations(cloud, remote.id) : null;
22241
+ const [commentPage, relations, gitRefs] = remote ? await Promise.all([
22242
+ cloudListComments(cloud, remote.id, page.request),
22243
+ cloudDetailRelations(cloud, remote.id),
22244
+ cloudDetailGitRefs(cloud, remote.id)
22245
+ ]) : [null, null, null];
21681
22246
  task2 = remote ? {
21682
22247
  subtasks: [],
21683
22248
  ...remote,
@@ -21685,6 +22250,7 @@ function registerTaskCommands(program2) {
21685
22250
  dependencies: relations.dependencies,
21686
22251
  blocked_by: relations.blocked_by,
21687
22252
  blocks: relations.blocks,
22253
+ git_refs: gitRefs,
21688
22254
  comments: commentPage.comments,
21689
22255
  comments_page: {
21690
22256
  count: commentPage.count,
@@ -21697,6 +22263,8 @@ function registerTaskCommands(program2) {
21697
22263
  } else {
21698
22264
  const resolvedId = resolveTaskId(id);
21699
22265
  task2 = applyLocalCommentPage(getTaskWithRelations(resolvedId), page);
22266
+ const { getTaskGitRefs: getTaskGitRefs2 } = await Promise.resolve().then(() => (init_task_commits(), exports_task_commits));
22267
+ task2.git_refs = getTaskGitRefs2(resolvedId);
21700
22268
  }
21701
22269
  if (!task2) {
21702
22270
  handleError(new Error(`Task not found: ${id}`));
@@ -21809,8 +22377,11 @@ function registerTaskCommands(program2) {
21809
22377
  let task2;
21810
22378
  if (cloud) {
21811
22379
  const remote = await cloudGetTask(cloud, resolvedId);
21812
- const commentPage = remote ? await cloudListComments(cloud, remote.id, page.request) : null;
21813
- const relations = remote ? await cloudDetailRelations(cloud, remote.id) : null;
22380
+ const [commentPage, relations, gitRefs] = remote ? await Promise.all([
22381
+ cloudListComments(cloud, remote.id, page.request),
22382
+ cloudDetailRelations(cloud, remote.id),
22383
+ cloudDetailGitRefs(cloud, remote.id)
22384
+ ]) : [null, null, null];
21814
22385
  task2 = remote ? {
21815
22386
  subtasks: [],
21816
22387
  checklist: [],
@@ -21819,6 +22390,7 @@ function registerTaskCommands(program2) {
21819
22390
  dependencies: relations.dependencies,
21820
22391
  blocked_by: relations.blocked_by,
21821
22392
  blocks: relations.blocks,
22393
+ git_refs: gitRefs,
21822
22394
  comments: commentPage.comments,
21823
22395
  comments_page: {
21824
22396
  count: commentPage.count,
@@ -21836,7 +22408,7 @@ function registerTaskCommands(program2) {
21836
22408
  }
21837
22409
  if (globalOpts.json && !cloud) {
21838
22410
  const { listTaskFiles: listTaskFiles2 } = await Promise.resolve().then(() => (init_task_files(), exports_task_files));
21839
- const { getTaskCommits: getTaskCommits2 } = await Promise.resolve().then(() => (init_task_commits(), exports_task_commits));
22411
+ const { getTaskCommits: getTaskCommits2, getTaskGitRefs: getTaskGitRefs2 } = await Promise.resolve().then(() => (init_task_commits(), exports_task_commits));
21840
22412
  try {
21841
22413
  task2.files = listTaskFiles2(task2.id);
21842
22414
  } catch (e) {
@@ -21847,6 +22419,11 @@ function registerTaskCommands(program2) {
21847
22419
  } catch (e) {
21848
22420
  console.error(chalk3.dim(`Warning: could not load task commits: ${e instanceof Error ? e.message : String(e)}`));
21849
22421
  }
22422
+ try {
22423
+ task2.git_refs = getTaskGitRefs2(task2.id);
22424
+ } catch (e) {
22425
+ console.error(chalk3.dim(`Warning: could not load task git refs: ${e instanceof Error ? e.message : String(e)}`));
22426
+ }
21850
22427
  output(task2, true);
21851
22428
  return;
21852
22429
  }
@@ -22840,62 +23417,6 @@ var init_plan_artifacts = __esm(() => {
22840
23417
  init_tasks();
22841
23418
  });
22842
23419
 
22843
- // src/lib/plan-project-link-contract.ts
22844
- import { createHash as createHash5 } from "crypto";
22845
- function canonicalPlanProjectLinkJson(value) {
22846
- if (value === null || typeof value !== "object")
22847
- return JSON.stringify(value);
22848
- if (Array.isArray(value))
22849
- return `[${value.map(canonicalPlanProjectLinkJson).join(",")}]`;
22850
- return `{${Object.entries(value).filter(([, item]) => item !== undefined).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${canonicalPlanProjectLinkJson(item)}`).join(",")}}`;
22851
- }
22852
- function planProjectLinkDigest(value) {
22853
- return createHash5("sha256").update(canonicalPlanProjectLinkJson(value)).digest("hex");
22854
- }
22855
- function normalizePlanProjectLinkIdempotencyKey(value) {
22856
- const key = value?.trim() ?? "";
22857
- if (key.length < 8 || key.length > 128 || !/^[A-Za-z0-9._:-]+$/.test(key)) {
22858
- throw new PlanProjectLinkError("PLAN_PROJECT_LINK_IDEMPOTENCY_KEY_INVALID", "idempotency_key must be 8-128 ASCII letters, digits, dots, underscores, colons, or hyphens");
22859
- }
22860
- return key;
22861
- }
22862
- function planProjectLinkReceiptId(idempotencyKey) {
22863
- return `pplr_${planProjectLinkDigest({ idempotency_key: idempotencyKey }).slice(0, 48)}`;
22864
- }
22865
- function planProjectLinkRollbackReceiptId(receiptId) {
22866
- return `pplr_inverse_${planProjectLinkDigest({ accepted_receipt_id: receiptId }).slice(0, 38)}`;
22867
- }
22868
- function planProjectLinkRequestHash(planId, projectId) {
22869
- return planProjectLinkDigest({ plan_id: planId, project_id: projectId });
22870
- }
22871
- function planProjectLinkResultDigest(plan, tasks) {
22872
- return planProjectLinkDigest({
22873
- plan_id: plan.id,
22874
- plan_project_id: plan.project_id,
22875
- tasks: tasks.map((task) => ({ id: task.id, plan_id: task.plan_id, project_id: task.project_id })).sort((left, right) => left.id.localeCompare(right.id))
22876
- });
22877
- }
22878
- function assertPlanProjectLinkReceipt(value) {
22879
- const receipt = value;
22880
- if (!receipt || typeof receipt !== "object" || receipt.schema_version !== PLAN_PROJECT_LINK_SCHEMA_VERSION || typeof receipt.receipt_id !== "string" || typeof receipt.idempotency_key !== "string" || typeof receipt.plan_id !== "string" || typeof receipt.project_id !== "string" || !Array.isArray(receipt.task_ids) || receipt.task_ids.some((id) => typeof id !== "string") || !receipt.prior_task_project_ids || typeof receipt.prior_task_project_ids !== "object" || typeof receipt.result_digest !== "string") {
22881
- throw new PlanProjectLinkError("PLAN_PROJECT_LINK_RECEIPT_NOT_FOUND", "Stored plan-project-link receipt is invalid");
22882
- }
22883
- return receipt;
22884
- }
22885
- var PLAN_PROJECT_LINK_SCHEMA_VERSION = "todos.plan-project-link.v1", PlanProjectLinkError;
22886
- var init_plan_project_link_contract = __esm(() => {
22887
- PlanProjectLinkError = class PlanProjectLinkError extends Error {
22888
- code;
22889
- details;
22890
- constructor(code, message, details = {}) {
22891
- super(message);
22892
- this.code = code;
22893
- this.details = details;
22894
- this.name = "PlanProjectLinkError";
22895
- }
22896
- };
22897
- });
22898
-
22899
23420
  // src/lib/plan-project-link.ts
22900
23421
  async function exactPlanProjectLinkState(store, planId, projectId) {
22901
23422
  const [plan, project] = await Promise.all([
@@ -40339,6 +40860,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
40339
40860
  ProjectTaskListEnsureResult: projectTaskListEnsureResultSchema,
40340
40861
  ProjectTaskListRollbackResult: projectTaskListRollbackResultSchema,
40341
40862
  TaskComment: taskCommentSchema,
40863
+ TaskGitRef: taskGitRefSchema,
40342
40864
  Plan: planSchema,
40343
40865
  PlanProjectLinkReceipt: planProjectLinkReceiptSchema,
40344
40866
  PlanProjectLinkResult: planProjectLinkResultSchema,
@@ -41500,6 +42022,92 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
41500
42022
  }
41501
42023
  }
41502
42024
  },
42025
+ "/v1/tasks/{id}/refs": {
42026
+ get: {
42027
+ operationId: "listTaskGitRefs",
42028
+ summary: "List git branch and pull-request refs linked to a task",
42029
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
42030
+ responses: {
42031
+ "200": {
42032
+ content: {
42033
+ "application/json": {
42034
+ schema: {
42035
+ type: "object",
42036
+ additionalProperties: false,
42037
+ required: ["refs", "count"],
42038
+ properties: {
42039
+ refs: { type: "array", items: { $ref: "#/components/schemas/TaskGitRef" } },
42040
+ count: { type: "integer", minimum: 0 }
42041
+ }
42042
+ }
42043
+ }
42044
+ }
42045
+ }
42046
+ }
42047
+ },
42048
+ post: {
42049
+ operationId: "linkTaskGitRef",
42050
+ summary: "Link a git branch or pull-request ref to a task",
42051
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
42052
+ requestBody: {
42053
+ required: true,
42054
+ content: {
42055
+ "application/json": {
42056
+ schema: {
42057
+ type: "object",
42058
+ additionalProperties: false,
42059
+ required: ["ref_type", "name"],
42060
+ properties: {
42061
+ ref_type: { type: "string", enum: ["branch", "pull_request"] },
42062
+ name: { type: "string", minLength: 1 },
42063
+ url: { type: "string" },
42064
+ provider: { type: "string" },
42065
+ metadata: { type: "object", additionalProperties: true }
42066
+ }
42067
+ }
42068
+ }
42069
+ }
42070
+ },
42071
+ responses: {
42072
+ "201": {
42073
+ content: {
42074
+ "application/json": {
42075
+ schema: {
42076
+ type: "object",
42077
+ additionalProperties: false,
42078
+ required: ["ref"],
42079
+ properties: { ref: { $ref: "#/components/schemas/TaskGitRef" } }
42080
+ }
42081
+ }
42082
+ }
42083
+ }
42084
+ }
42085
+ }
42086
+ },
42087
+ "/v1/refs/{ref}": {
42088
+ get: {
42089
+ operationId: "findTaskGitRefs",
42090
+ summary: "Find task links by git branch or pull-request ref",
42091
+ parameters: [{ name: "ref", in: "path", required: true, schema: { type: "string" } }],
42092
+ responses: {
42093
+ "200": {
42094
+ content: {
42095
+ "application/json": {
42096
+ schema: {
42097
+ type: "object",
42098
+ additionalProperties: false,
42099
+ required: ["refs", "count"],
42100
+ properties: {
42101
+ refs: { type: "array", items: { $ref: "#/components/schemas/TaskGitRef" } },
42102
+ count: { type: "integer", minimum: 0 }
42103
+ }
42104
+ }
42105
+ }
42106
+ }
42107
+ }
42108
+ }
42109
+ }
42110
+ },
41503
42111
  "/v1/tasks/{id}/start": {
41504
42112
  post: {
41505
42113
  operationId: "startTask",
@@ -41951,7 +42559,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
41951
42559
  }
41952
42560
  };
41953
42561
  }
41954
- var taskSchema, projectSchema, taskListSchema, projectTaskListEnsureReceiptSchema, projectTaskListEnsureResultSchema, projectTaskListRollbackResultSchema, taskCommentSchema, planSchema, planProjectLinkReceiptSchema, planProjectLinkResultSchema, planProjectLinkRollbackResultSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
42562
+ var taskSchema, projectSchema, taskListSchema, projectTaskListEnsureReceiptSchema, projectTaskListEnsureResultSchema, projectTaskListRollbackResultSchema, taskCommentSchema, taskGitRefSchema, planSchema, planProjectLinkReceiptSchema, planProjectLinkResultSchema, planProjectLinkRollbackResultSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
41955
42563
  var init_openapi = __esm(() => {
41956
42564
  init_package_version();
41957
42565
  init_types();
@@ -42087,6 +42695,32 @@ var init_openapi = __esm(() => {
42087
42695
  created_at: { type: "string", format: "date-time" }
42088
42696
  }
42089
42697
  };
42698
+ taskGitRefSchema = {
42699
+ type: "object",
42700
+ additionalProperties: false,
42701
+ required: [
42702
+ "id",
42703
+ "task_id",
42704
+ "ref_type",
42705
+ "name",
42706
+ "url",
42707
+ "provider",
42708
+ "metadata",
42709
+ "created_at",
42710
+ "updated_at"
42711
+ ],
42712
+ properties: {
42713
+ id: { type: "string", minLength: 1 },
42714
+ task_id: { type: "string", minLength: 1 },
42715
+ ref_type: { type: "string", enum: ["branch", "pull_request"] },
42716
+ name: { type: "string", minLength: 1 },
42717
+ url: { type: "string", nullable: true },
42718
+ provider: { type: "string", nullable: true },
42719
+ metadata: { type: "object", additionalProperties: true },
42720
+ created_at: { type: "string", format: "date-time" },
42721
+ updated_at: { type: "string", format: "date-time" }
42722
+ }
42723
+ };
42090
42724
  planSchema = {
42091
42725
  type: "object",
42092
42726
  required: ["id", "slug", "name", "status", "created_at", "updated_at"],