@hasna/todos 0.15.15 → 0.15.17

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/mcp/index.js CHANGED
@@ -13596,8 +13596,8 @@ function failTask(id, agentId, reason, options, db) {
13596
13596
  const safeMeta = sanitizePreWriteValue(meta, "task.failure.metadata");
13597
13597
  const timestamp2 = now();
13598
13598
  const failTx = d.transaction(() => {
13599
- const res = d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
13600
- WHERE id = ? AND version = ?`, [JSON.stringify(safeMeta), timestamp2, id, task.version]);
13599
+ const res = d.run(`UPDATE tasks SET status = 'failed', reason = ?, locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
13600
+ WHERE id = ? AND version = ?`, [safeReason, JSON.stringify(safeMeta), timestamp2, id, task.version]);
13601
13601
  if (res.changes === 0) {
13602
13602
  const current = getTask(id, d);
13603
13603
  throw new VersionConflictError(id, task.version, current?.version ?? -1);
@@ -13609,6 +13609,7 @@ function failTask(id, agentId, reason, options, db) {
13609
13609
  status: "failed",
13610
13610
  locked_by: null,
13611
13611
  locked_at: null,
13612
+ reason: safeReason,
13612
13613
  metadata: safeMeta,
13613
13614
  version: task.version + 1,
13614
13615
  updated_at: timestamp2
@@ -21041,10 +21042,72 @@ async function requireTagsFilterCapability(client) {
21041
21042
  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");
21042
21043
  }
21043
21044
  }
21044
- async function requestCloudTaskPage(client, filter) {
21045
+ function parseCloudTaskTotal(raw) {
21046
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
21047
+ return;
21048
+ const total = raw.total;
21049
+ return typeof total === "number" && Number.isSafeInteger(total) && total >= 0 ? total : undefined;
21050
+ }
21051
+ async function requestRawCloudTaskPage(client, filter) {
21045
21052
  const res = await requiredRemoteRoute(client, "/v1/tasks", () => client.list("tasks", { query: toListQuery(filter) }));
21046
21053
  const envelope = res.raw;
21047
- return Array.isArray(envelope?.tasks) ? envelope.tasks : res.items;
21054
+ return {
21055
+ tasks: Array.isArray(envelope?.tasks) ? envelope.tasks : res.items,
21056
+ total: parseCloudTaskTotal(res.raw)
21057
+ };
21058
+ }
21059
+ function cloudTaskListFilterError(code, taskListId, detail) {
21060
+ return new Error(`${code}: hosted authority returned rows outside requested task_list_id ${taskListId}; ${detail}; refusing an incomplete exact-list result`);
21061
+ }
21062
+ async function requestCloudTaskPage(client, filter) {
21063
+ const firstPage = await requestRawCloudTaskPage(client, filter);
21064
+ const tasks = firstPage.tasks;
21065
+ if (filter.task_list_id === undefined)
21066
+ return tasks;
21067
+ const taskListId = filter.task_list_id;
21068
+ if (tasks.every((task) => task.task_list_id === taskListId))
21069
+ return tasks;
21070
+ const scanLimit = filter.limit;
21071
+ const startOffset = filter.offset ?? 0;
21072
+ if (startOffset !== 0 || scanLimit === undefined || !Number.isSafeInteger(scanLimit) || scanLimit <= 0 || firstPage.total === undefined) {
21073
+ throw cloudTaskListFilterError("REMOTE_TASK_LIST_FILTER_UNSUPPORTED", taskListId, "the response does not provide complete bounded total/offset pagination evidence");
21074
+ }
21075
+ const total = firstPage.total;
21076
+ if (total > scanLimit) {
21077
+ throw cloudTaskListFilterError("REMOTE_TASK_LIST_FILTER_INCOMPLETE", taskListId, `reported total ${total} exceeds bounded scan limit ${scanLimit}`);
21078
+ }
21079
+ if (tasks.length > total) {
21080
+ throw cloudTaskListFilterError("REMOTE_TASK_LIST_FILTER_UNSUPPORTED", taskListId, `the first page contains ${tasks.length} rows but reports total ${total}`);
21081
+ }
21082
+ const seenTaskIds = new Set;
21083
+ for (const task of tasks) {
21084
+ if (seenTaskIds.has(task.id)) {
21085
+ throw cloudTaskListFilterError("REMOTE_TASK_LIST_FILTER_UNSUPPORTED", taskListId, `the first page repeats task id ${task.id}`);
21086
+ }
21087
+ seenTaskIds.add(task.id);
21088
+ }
21089
+ while (tasks.length < total) {
21090
+ const remaining = total - tasks.length;
21091
+ const page = await requestRawCloudTaskPage(client, {
21092
+ ...filter,
21093
+ offset: tasks.length,
21094
+ limit: Math.min(scanLimit - tasks.length, remaining)
21095
+ });
21096
+ if (page.total !== total) {
21097
+ throw cloudTaskListFilterError("REMOTE_TASK_LIST_FILTER_UNSUPPORTED", taskListId, `pagination total changed from ${total} to ${String(page.total)}`);
21098
+ }
21099
+ if (page.tasks.length === 0 || tasks.length + page.tasks.length > total) {
21100
+ throw cloudTaskListFilterError("REMOTE_TASK_LIST_FILTER_UNSUPPORTED", taskListId, "pagination did not make bounded progress toward the reported total");
21101
+ }
21102
+ for (const task of page.tasks) {
21103
+ if (seenTaskIds.has(task.id)) {
21104
+ throw cloudTaskListFilterError("REMOTE_TASK_LIST_FILTER_UNSUPPORTED", taskListId, `pagination repeats task id ${task.id}`);
21105
+ }
21106
+ seenTaskIds.add(task.id);
21107
+ }
21108
+ tasks.push(...page.tasks);
21109
+ }
21110
+ return tasks.filter((task) => task.task_list_id === taskListId);
21048
21111
  }
21049
21112
  async function cloudListTasks(client, filter = {}) {
21050
21113
  if (filter.tags?.length)
@@ -21083,12 +21146,10 @@ async function cloudGetTask(client, id) {
21083
21146
  }
21084
21147
  async function cloudCreateTask(client, input) {
21085
21148
  const expectedParentId = typeof input["parent_id"] === "string" ? input["parent_id"] : null;
21086
- const created = unwrapTask(await requiredRemoteRoute(client, "/v1/tasks", () => expectedParentId === null ? client.create("tasks", input) : client.create("tasks", input, { retry: false }), ["PARENT_TASK_NOT_FOUND"]));
21149
+ const created = unwrapTask(await requiredRemoteRoute(client, "/v1/tasks", () => client.create("tasks", input, { retry: false }), ["PARENT_TASK_NOT_FOUND"]));
21087
21150
  if (!created || typeof created.id !== "string" || !created.id.trim()) {
21088
21151
  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");
21089
21152
  }
21090
- if (expectedParentId === null)
21091
- return created;
21092
21153
  const persisted = await cloudGetTask(client, created.id);
21093
21154
  if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== expectedParentId) {
21094
21155
  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; no success row or local SQLite fallback is permitted");
@@ -35415,7 +35476,7 @@ var package_default;
35415
35476
  var init_package = __esm(() => {
35416
35477
  package_default = {
35417
35478
  name: "@hasna/todos",
35418
- version: "0.15.15",
35479
+ version: "0.15.17",
35419
35480
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
35420
35481
  type: "module",
35421
35482
  main: "dist/index.js",
@@ -51606,6 +51667,22 @@ class PostgresTodosTaskManifestBackend {
51606
51667
  async markOutboxDelivered(outboxId, deliveredAt) {
51607
51668
  await this.ensureSchema();
51608
51669
  await this.client.transaction(async (tx) => {
51670
+ const owned = await tx.query(`SELECT r.operation_id
51671
+ FROM todos_task_manifest_outbox o
51672
+ JOIN todos_task_manifest_receipts r
51673
+ ON r.receipt_id = o.apply_receipt_id
51674
+ WHERE r.tenant_id = $1
51675
+ AND r.authority = 'todos'
51676
+ AND r.route = 'todos.task-manifest.v1'
51677
+ AND r.schema_version = 1
51678
+ AND r.kind = 'apply'
51679
+ AND o.id = $2
51680
+ LIMIT 1`, [this.tenantId, outboxId]);
51681
+ const operationId = owned.rows[0]?.operation_id;
51682
+ if (operationId == null) {
51683
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", `Pending outbox row not found: ${outboxId}`);
51684
+ }
51685
+ await tx.query("SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [`${this.service}\x1F${String(operationId)}`]);
51609
51686
  const result = await tx.query(`UPDATE todos_task_manifest_outbox
51610
51687
  SET status = 'delivered', delivered_at = $1, attempts = attempts + 1
51611
51688
  WHERE id = $2 AND status = 'pending'
@@ -51613,10 +51690,28 @@ class PostgresTodosTaskManifestBackend {
51613
51690
  SELECT 1 FROM todos_task_manifest_receipts r
51614
51691
  WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
51615
51692
  AND r.tenant_id = $3
51693
+ AND r.authority = 'todos'
51694
+ AND r.route = 'todos.task-manifest.v1'
51695
+ AND r.schema_version = 1
51696
+ AND r.kind = 'apply'
51616
51697
  )
51617
51698
  RETURNING id`, [deliveredAt, outboxId, this.tenantId]);
51618
- if (!result.rows[0])
51619
- throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", `Pending outbox row not found: ${outboxId}`);
51699
+ if (result.rows[0])
51700
+ return;
51701
+ const existing = await tx.query(`SELECT o.status
51702
+ FROM todos_task_manifest_outbox o
51703
+ JOIN todos_task_manifest_receipts r
51704
+ ON r.receipt_id = o.apply_receipt_id
51705
+ WHERE r.tenant_id = $1
51706
+ AND r.authority = 'todos'
51707
+ AND r.route = 'todos.task-manifest.v1'
51708
+ AND r.schema_version = 1
51709
+ AND r.kind = 'apply'
51710
+ AND o.id = $2
51711
+ LIMIT 1`, [this.tenantId, outboxId]);
51712
+ if (existing.rows[0]?.status === "delivered")
51713
+ return;
51714
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_GRAPH_CONFLICT", `Pending outbox row not found: ${outboxId}`);
51620
51715
  });
51621
51716
  }
51622
51717
  async compensate(input, receipt, compensationReceiptId, requestDigest, now4) {
@@ -51648,7 +51743,13 @@ class PostgresTodosTaskManifestBackend {
51648
51743
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Graph is not applied");
51649
51744
  const delivered = await tx.query(`SELECT o.id FROM todos_task_manifest_outbox o
51650
51745
  JOIN todos_task_manifest_receipts r ON r.receipt_id = o.apply_receipt_id
51651
- WHERE r.tenant_id = $1 AND o.apply_receipt_id = $2 AND o.status = 'delivered'
51746
+ WHERE r.tenant_id = $1
51747
+ AND r.authority = 'todos'
51748
+ AND r.route = 'todos.task-manifest.v1'
51749
+ AND r.schema_version = 1
51750
+ AND r.kind = 'apply'
51751
+ AND o.apply_receipt_id = $2
51752
+ AND o.status = 'delivered'
51652
51753
  LIMIT 1`, [this.tenantId, input.receipt_id]);
51653
51754
  if (delivered.rows[0])
51654
51755
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: delivered outbox row exists");
@@ -51661,15 +51762,20 @@ class PostgresTodosTaskManifestBackend {
51661
51762
  },
51662
51763
  ...(manifest.effects ?? []).map((effect2) => ({ topic: effect2.topic, payload: effect2.payload }))
51663
51764
  ];
51664
- const outboxRows = await tx.query(`SELECT id, topic, payload, payload_digest, status, attempts, delivered_at
51665
- FROM todos_task_manifest_outbox
51666
- WHERE apply_receipt_id = $1
51765
+ const outboxRows = await tx.query(`SELECT o.id, o.topic, o.payload, o.payload_digest, o.status, o.attempts, o.delivered_at
51766
+ FROM todos_task_manifest_outbox o
51767
+ WHERE o.apply_receipt_id = $1
51667
51768
  AND EXISTS (
51668
51769
  SELECT 1 FROM todos_task_manifest_receipts r
51669
- WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
51770
+ WHERE r.receipt_id = o.apply_receipt_id
51670
51771
  AND r.tenant_id = $2
51772
+ AND r.authority = 'todos'
51773
+ AND r.route = 'todos.task-manifest.v1'
51774
+ AND r.schema_version = 1
51775
+ AND r.kind = 'apply'
51671
51776
  )
51672
- ORDER BY id`, [input.receipt_id, this.tenantId]);
51777
+ ORDER BY o.id
51778
+ FOR UPDATE OF o`, [input.receipt_id, this.tenantId]);
51673
51779
  if (outboxRows.rows.length !== expectedEffects.length) {
51674
51780
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: outbox changed since apply");
51675
51781
  }
@@ -51769,14 +51875,23 @@ class PostgresTodosTaskManifestBackend {
51769
51875
  }
51770
51876
  if (stored.rows.length !== managedIds.length)
51771
51877
  throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: managed graph is incomplete");
51772
- await tx.query(`UPDATE todos_task_manifest_outbox
51878
+ const cancelled = await tx.query(`UPDATE todos_task_manifest_outbox
51773
51879
  SET status = 'cancelled'
51774
51880
  WHERE apply_receipt_id = $1 AND status = 'pending'
51775
51881
  AND EXISTS (
51776
51882
  SELECT 1 FROM todos_task_manifest_receipts r
51777
51883
  WHERE r.receipt_id = todos_task_manifest_outbox.apply_receipt_id
51778
51884
  AND r.tenant_id = $2
51779
- )`, [input.receipt_id, this.tenantId]);
51885
+ AND r.authority = 'todos'
51886
+ AND r.route = 'todos.task-manifest.v1'
51887
+ AND r.schema_version = 1
51888
+ AND r.kind = 'apply'
51889
+ )
51890
+ RETURNING id`, [input.receipt_id, this.tenantId]);
51891
+ const cancelledIds = new Set(cancelled.rows.map((row) => String(row.id)));
51892
+ if (cancelledIds.size !== applyResult.outbox_ids.length || applyResult.outbox_ids.some((id) => !cancelledIds.has(id))) {
51893
+ throw new TodosTaskManifestError("TODOS_TASK_MANIFEST_COMPENSATION_REFUSED", "Compensation refused: failed to cancel every expected outbox row");
51894
+ }
51780
51895
  const typedIds = [
51781
51896
  ["dependencies", applyResult.graph.dependency_ids],
51782
51897
  ["comments", applyResult.graph.comment_ids],
@@ -51959,6 +52074,7 @@ class PackageOwnedTodosTaskManifestAuthority {
51959
52074
  deterministic_ids: true,
51960
52075
  immutable_receipts: true,
51961
52076
  transactional_outbox: true,
52077
+ idempotent_outbox_delivery: true,
51962
52078
  exact_bounded_readback: true,
51963
52079
  conditional_compensation: true,
51964
52080
  transcript_safe: false,
@@ -53715,8 +53831,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
53715
53831
  properties: {
53716
53832
  title: { type: "string" },
53717
53833
  description: { type: "string", nullable: true },
53718
- status: { type: "string" },
53719
- priority: { type: "string" },
53834
+ status: { type: "string", enum: [...TASK_STATUSES] },
53835
+ priority: { type: "string", enum: [...TASK_PRIORITIES] },
53720
53836
  project_id: { type: "string" },
53721
53837
  parent_id: { type: "string" },
53722
53838
  plan_id: { type: "string" },
@@ -53730,8 +53846,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
53730
53846
  properties: {
53731
53847
  title: { type: "string" },
53732
53848
  description: { type: "string" },
53733
- status: { type: "string" },
53734
- priority: { type: "string" },
53849
+ status: { type: "string", enum: [...TASK_STATUSES] },
53850
+ priority: { type: "string", enum: [...TASK_PRIORITIES] },
53735
53851
  assigned_to: { type: "string" },
53736
53852
  project_id: { type: "string", nullable: true },
53737
53853
  plan_id: { type: "string", nullable: true },
@@ -53752,6 +53868,24 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
53752
53868
  confidence: { type: "number", minimum: 0, maximum: 1 }
53753
53869
  }
53754
53870
  },
53871
+ FailTaskInput: {
53872
+ type: "object",
53873
+ additionalProperties: false,
53874
+ properties: {
53875
+ agent_id: { type: "string", minLength: 1 },
53876
+ reason: { type: "string" },
53877
+ retry: { type: "boolean" }
53878
+ }
53879
+ },
53880
+ TaskFailureResult: {
53881
+ type: "object",
53882
+ additionalProperties: false,
53883
+ required: ["task"],
53884
+ properties: {
53885
+ task: { $ref: "#/components/schemas/Task" },
53886
+ retryTask: { $ref: "#/components/schemas/Task" }
53887
+ }
53888
+ },
53755
53889
  CreateProjectInput: {
53756
53890
  type: "object",
53757
53891
  additionalProperties: false,
@@ -54995,6 +55129,30 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
54995
55129
  responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { task: { $ref: "#/components/schemas/Task" } } } } } } }
54996
55130
  }
54997
55131
  },
55132
+ "/v1/tasks/{id}/fail": {
55133
+ post: {
55134
+ operationId: "failTask",
55135
+ summary: "Fail a task with an optional reason and retry copy",
55136
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
55137
+ requestBody: {
55138
+ required: false,
55139
+ content: { "application/json": { schema: { $ref: "#/components/schemas/FailTaskInput" } } }
55140
+ },
55141
+ responses: {
55142
+ "200": {
55143
+ content: {
55144
+ "application/json": {
55145
+ schema: {
55146
+ type: "object",
55147
+ required: ["result"],
55148
+ properties: { result: { $ref: "#/components/schemas/TaskFailureResult" } }
55149
+ }
55150
+ }
55151
+ }
55152
+ }
55153
+ }
55154
+ }
55155
+ },
54998
55156
  "/v1/projects": {
54999
55157
  get: {
55000
55158
  operationId: "listProjects",
@@ -55436,12 +55594,13 @@ var init_openapi = __esm(() => {
55436
55594
  id: { type: "string" },
55437
55595
  title: { type: "string" },
55438
55596
  description: { type: "string" },
55439
- status: { type: "string" },
55440
- priority: { type: "string" },
55597
+ status: { type: "string", enum: [...TASK_STATUSES] },
55598
+ priority: { type: "string", enum: [...TASK_PRIORITIES] },
55441
55599
  project_id: { type: "string", nullable: true },
55442
55600
  parent_id: { type: "string", nullable: true },
55443
55601
  assigned_to: { type: "string", nullable: true },
55444
55602
  agent_id: { type: "string", nullable: true },
55603
+ reason: { type: "string", nullable: true },
55445
55604
  tags: { type: "array", items: { type: "string" } },
55446
55605
  version: { type: "number" },
55447
55606
  created_at: { type: "string" },
@@ -56321,6 +56480,44 @@ function validateTaskCompletion(value) {
56321
56480
  }
56322
56481
  };
56323
56482
  }
56483
+ function validateTaskFailure(value) {
56484
+ if (!value || typeof value !== "object" || Array.isArray(value))
56485
+ return { ok: false, message: "failure body must be an object" };
56486
+ const body2 = value;
56487
+ const allowed = new Set(["agent_id", "reason", "retry"]);
56488
+ const unknown = Object.keys(body2).find((key2) => !allowed.has(key2));
56489
+ if (unknown)
56490
+ return { ok: false, message: `unknown failure field: ${unknown}` };
56491
+ if (body2.agent_id !== undefined && (typeof body2.agent_id !== "string" || !body2.agent_id.trim())) {
56492
+ return { ok: false, message: "agent_id must be a non-empty string" };
56493
+ }
56494
+ if (body2.reason !== undefined && typeof body2.reason !== "string")
56495
+ return { ok: false, message: "reason must be a string" };
56496
+ if (body2.retry !== undefined && typeof body2.retry !== "boolean")
56497
+ return { ok: false, message: "retry must be a boolean" };
56498
+ return {
56499
+ ok: true,
56500
+ ...typeof body2.agent_id === "string" ? { agentId: body2.agent_id } : {},
56501
+ reason: typeof body2.reason === "string" && body2.reason ? body2.reason : "Unknown failure",
56502
+ retry: body2.retry === true
56503
+ };
56504
+ }
56505
+ function validateTaskPatchVocabulary(value) {
56506
+ if (!value || typeof value !== "object" || Array.isArray(value))
56507
+ return { ok: false, message: "task patch must be an object" };
56508
+ const body2 = value;
56509
+ for (const [name, vocabulary] of [["status", TASK_STATUSES], ["priority", TASK_PRIORITIES]]) {
56510
+ const raw = body2[name];
56511
+ if (raw === undefined)
56512
+ continue;
56513
+ if (typeof raw !== "string")
56514
+ return { ok: false, message: `${name} must be a string. Allowed values: ${vocabulary.join(", ")}.` };
56515
+ const parsed = resolveEnumVocabulary(raw, { name, vocabulary, allowList: false });
56516
+ if (!parsed.ok)
56517
+ return { ok: false, message: parsed.message };
56518
+ }
56519
+ return { ok: true, patch: body2 };
56520
+ }
56324
56521
  function validateProjectPatch(value) {
56325
56522
  if (!value || typeof value !== "object" || Array.isArray(value))
56326
56523
  return { ok: false, message: "project patch must be an object" };
@@ -57001,7 +57198,12 @@ async function handleV1Request(req, url, dependencies = {}) {
57001
57198
  });
57002
57199
  }
57003
57200
  if (action === "fail" && method === "POST") {
57004
- return json5({ result: await store.tasks.fail(id, agentId, typeof body2.reason === "string" ? body2.reason : "failed", {}) });
57201
+ const parsed = validateTaskFailure(actionJson.value);
57202
+ if (!parsed.ok)
57203
+ return error(400, parsed.message);
57204
+ return json5({
57205
+ result: await store.tasks.fail(id, parsed.agentId || principal.agent || "todos-serve", parsed.reason, { retry: parsed.retry }, contextFromPrincipal(principal, body2))
57206
+ });
57005
57207
  }
57006
57208
  if (action === "claim" && method === "POST") {
57007
57209
  return json5({ task: await store.tasks.claimNext(agentId, {}) });
@@ -57030,9 +57232,13 @@ async function handleV1Request(req, url, dependencies = {}) {
57030
57232
  return task3 ? json5({ task: task3 }) : error(404, "task not found");
57031
57233
  }
57032
57234
  if (method === "PATCH" || method === "PUT") {
57033
- const body2 = await readJson3(req);
57034
- if (!body2)
57235
+ const rawBody = await readJson3(req);
57236
+ if (!rawBody)
57035
57237
  return error(400, "invalid JSON body");
57238
+ const validated = validateTaskPatchVocabulary(rawBody);
57239
+ if (!validated.ok)
57240
+ return error(400, validated.message);
57241
+ const body2 = validated.patch;
57036
57242
  const current = await store.tasks.get(id);
57037
57243
  if (!current)
57038
57244
  return error(404, "task not found");
package/dist/mcp.js CHANGED
@@ -41,7 +41,7 @@ var __require = import.meta.require;
41
41
  // package.json
42
42
  var package_default = {
43
43
  name: "@hasna/todos",
44
- version: "0.15.15",
44
+ version: "0.15.17",
45
45
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
46
46
  type: "module",
47
47
  main: "dist/index.js",
@@ -8949,8 +8949,8 @@ function failTask2(id, agentId, reason, options, db) {
8949
8949
  const safeMeta = sanitizePreWriteValue(meta, "task.failure.metadata");
8950
8950
  const timestamp2 = now();
8951
8951
  const failTx = d.transaction(() => {
8952
- const res = d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
8953
- WHERE id = ? AND version = ?`, [JSON.stringify(safeMeta), timestamp2, id, task.version]);
8952
+ const res = d.run(`UPDATE tasks SET status = 'failed', reason = ?, locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
8953
+ WHERE id = ? AND version = ?`, [safeReason, JSON.stringify(safeMeta), timestamp2, id, task.version]);
8954
8954
  if (res.changes === 0) {
8955
8955
  const current = getTask(id, d);
8956
8956
  throw new VersionConflictError(id, task.version, current?.version ?? -1);
@@ -8962,6 +8962,7 @@ function failTask2(id, agentId, reason, options, db) {
8962
8962
  status: "failed",
8963
8963
  locked_by: null,
8964
8964
  locked_at: null,
8965
+ reason: safeReason,
8965
8966
  metadata: safeMeta,
8966
8967
  version: task.version + 1,
8967
8968
  updated_at: timestamp2
@@ -12382,7 +12383,7 @@ import { createHash as createHash5 } from "crypto";
12382
12383
  // package.json
12383
12384
  var package_default = {
12384
12385
  name: "@hasna/todos",
12385
- version: "0.15.15",
12386
+ version: "0.15.17",
12386
12387
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
12387
12388
  type: "module",
12388
12389
  main: "dist/index.js",
package/dist/registry.js CHANGED
@@ -8949,8 +8949,8 @@ function failTask(id, agentId, reason, options, db) {
8949
8949
  const safeMeta = sanitizePreWriteValue(meta, "task.failure.metadata");
8950
8950
  const timestamp2 = now();
8951
8951
  const failTx = d.transaction(() => {
8952
- const res = d.run(`UPDATE tasks SET status = 'failed', locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
8953
- WHERE id = ? AND version = ?`, [JSON.stringify(safeMeta), timestamp2, id, task.version]);
8952
+ const res = d.run(`UPDATE tasks SET status = 'failed', reason = ?, locked_by = NULL, locked_at = NULL, metadata = ?, version = version + 1, updated_at = ?
8953
+ WHERE id = ? AND version = ?`, [safeReason, JSON.stringify(safeMeta), timestamp2, id, task.version]);
8954
8954
  if (res.changes === 0) {
8955
8955
  const current = getTask(id, d);
8956
8956
  throw new VersionConflictError(id, task.version, current?.version ?? -1);
@@ -8962,6 +8962,7 @@ function failTask(id, agentId, reason, options, db) {
8962
8962
  status: "failed",
8963
8963
  locked_by: null,
8964
8964
  locked_at: null,
8965
+ reason: safeReason,
8965
8966
  metadata: safeMeta,
8966
8967
  version: task.version + 1,
8967
8968
  updated_at: timestamp2
@@ -12379,7 +12380,7 @@ var init_tasks = __esm(() => {
12379
12380
  // package.json
12380
12381
  var package_default = {
12381
12382
  name: "@hasna/todos",
12382
- version: "0.15.15",
12383
+ version: "0.15.17",
12383
12384
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
12384
12385
  type: "module",
12385
12386
  main: "dist/index.js",
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "packageName": "@hasna/todos",
3
- "packageVersion": "0.15.15",
3
+ "packageVersion": "0.15.17",
4
4
  "repository": "https://github.com/hasna/todos.git",
5
- "gitCommit": "3c9e40955ed30592a313258561b6cc76b928080e",
6
- "gitTree": "372cf75a8993b31e328a995694c2f7a4c49fc63c",
7
- "sourceTreeSha256": "95cf4fe5ee9dff0b651b37d4a499b714112a0b1a2f13a7eb326d5b79d1a3d77b",
8
- "generatedAt": "2026-08-08T18:01:52.000Z"
5
+ "gitCommit": "f5ad4d8f466d76f7b213b4285cb7eb8be1d65057",
6
+ "gitTree": "5a91a08de2e8af7d18d37de3dfa5956b141a62ac",
7
+ "sourceTreeSha256": "798227cadfbde7ad824600dd06968ca9b7c01f4f8a967f9f6e149b75de6dae93",
8
+ "generatedAt": "2026-08-09T03:22:03.000Z"
9
9
  }
@@ -10,7 +10,7 @@
10
10
  export { TodosClient, createClient } from "./client.js";
11
11
  export type { TodosClientOptions } from "./client.js";
12
12
  export { TodosV1Client, ApiError as TodosV1ApiError } from "./v1.generated.js";
13
- export type { TodosV1ClientOptions, Task as TodosV1Task, Project as TodosV1Project, TaskManifestBindingLookupRequest as TodosV1TaskManifestBindingLookupRequest, TaskManifestBindingLookupResult as TodosV1TaskManifestBindingLookupResult, TaskManifestBindingLookupResponse as TodosV1TaskManifestBindingLookupResponse, TaskComment as TodosV1TaskComment, CreateTaskInput as TodosV1CreateTaskInput, UpdateTaskInput as TodosV1UpdateTaskInput, CreateProjectInput as TodosV1CreateProjectInput, ProjectTaskListEnsureApplyInput as TodosV1ProjectTaskListEnsureApplyInput, ProjectTaskListRollbackInput as TodosV1ProjectTaskListRollbackInput, ProjectTaskListEnsureReceipt as TodosV1ProjectTaskListEnsureReceipt, ProjectTaskListEnsureResult as TodosV1ProjectTaskListEnsureResult, ProjectTaskListRollbackResult as TodosV1ProjectTaskListRollbackResult, CreateTaskCommentInput as TodosV1CreateTaskCommentInput, AdmitPrGroupInput as TodosV1AdmitPrGroupInput, RecoverPrGroupInput as TodosV1RecoverPrGroupInput, AppendPrGroupEventInput as TodosV1AppendPrGroupEventInput, PrGroupCiProof as TodosV1PrGroupCiProof, PrGroupCleanupProof as TodosV1PrGroupCleanupProof, PrGroupRecord as TodosV1PrGroupRecord, PrGroupAttemptRecord as TodosV1PrGroupAttemptRecord, PrGroupEventRecord as TodosV1PrGroupEventRecord, PrGroupStateView as TodosV1PrGroupStateView, PrGroupEventPage as TodosV1PrGroupEventPage, PrGroupMutationResult as TodosV1PrGroupMutationResult, } from "./v1.generated.js";
13
+ export type { TodosV1ClientOptions, Task as TodosV1Task, Project as TodosV1Project, TaskManifestBindingLookupRequest as TodosV1TaskManifestBindingLookupRequest, TaskManifestBindingLookupResult as TodosV1TaskManifestBindingLookupResult, TaskManifestBindingLookupResponse as TodosV1TaskManifestBindingLookupResponse, TaskComment as TodosV1TaskComment, CreateTaskInput as TodosV1CreateTaskInput, UpdateTaskInput as TodosV1UpdateTaskInput, FailTaskInput as TodosV1FailTaskInput, TaskFailureResult as TodosV1TaskFailureResult, CreateProjectInput as TodosV1CreateProjectInput, ProjectTaskListEnsureApplyInput as TodosV1ProjectTaskListEnsureApplyInput, ProjectTaskListRollbackInput as TodosV1ProjectTaskListRollbackInput, ProjectTaskListEnsureReceipt as TodosV1ProjectTaskListEnsureReceipt, ProjectTaskListEnsureResult as TodosV1ProjectTaskListEnsureResult, ProjectTaskListRollbackResult as TodosV1ProjectTaskListRollbackResult, CreateTaskCommentInput as TodosV1CreateTaskCommentInput, AdmitPrGroupInput as TodosV1AdmitPrGroupInput, RecoverPrGroupInput as TodosV1RecoverPrGroupInput, AppendPrGroupEventInput as TodosV1AppendPrGroupEventInput, PrGroupCiProof as TodosV1PrGroupCiProof, PrGroupCleanupProof as TodosV1PrGroupCleanupProof, PrGroupRecord as TodosV1PrGroupRecord, PrGroupAttemptRecord as TodosV1PrGroupAttemptRecord, PrGroupEventRecord as TodosV1PrGroupEventRecord, PrGroupStateView as TodosV1PrGroupStateView, PrGroupEventPage as TodosV1PrGroupEventPage, PrGroupMutationResult as TodosV1PrGroupMutationResult, } from "./v1.generated.js";
14
14
  export type { AdmitPrGroupInput, AppendPrGroupEventInput, PrGroupAdapterViews, PrGroupAttemptRecord, PrGroupCiProof, PrGroupCleanupProof, PrGroupDecisionEnvelopeAdapter, PrGroupEventListOptions, PrGroupEventPage, PrGroupEventRecord, PrGroupEvidenceRefAdapter, PrGroupMutationResult, PrGroupProofBundleAdapter, PrGroupRecord, PrGroupStateView, PrGroupWorkRunAdapter, RecoverPrGroupInput, } from "../pr-groups/types.js";
15
15
  export { TodosAPIError, TodosNotFoundError, TodosConflictError, TodosUnauthorizedError, TodosRateLimitError, TodosTimeoutError, } from "./types.js";
16
16
  export type { SSEEvent, CursorPage, ListOptions, TaskListResponse, TaskStatusResponse, TaskNextResponse, TaskActiveResponse, TaskStaleResponse, TaskChangedResponse, TaskContextResponse, TaskProgressResponse, TaskAttachmentsResponse, TaskFailResponse, TaskBulkResponse, AgentMeResponse, AgentQueueResponse, OrgNode, PlanWithTasks, ReportResponse, DoctorResponse, DoctorIssue, } from "./types.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sdk/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,YAAY,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAItD,OAAO,EAAE,aAAa,EAAE,QAAQ,IAAI,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAC/E,YAAY,EACV,oBAAoB,EACpB,IAAI,IAAI,WAAW,EACnB,OAAO,IAAI,cAAc,EACzB,gCAAgC,IAAI,uCAAuC,EAC3E,+BAA+B,IAAI,sCAAsC,EACzE,iCAAiC,IAAI,wCAAwC,EAC7E,WAAW,IAAI,kBAAkB,EACjC,eAAe,IAAI,sBAAsB,EACzC,eAAe,IAAI,sBAAsB,EACzC,kBAAkB,IAAI,yBAAyB,EAC/C,+BAA+B,IAAI,sCAAsC,EACzE,4BAA4B,IAAI,mCAAmC,EACnE,4BAA4B,IAAI,mCAAmC,EACnE,2BAA2B,IAAI,kCAAkC,EACjE,6BAA6B,IAAI,oCAAoC,EACrE,sBAAsB,IAAI,6BAA6B,EACvD,iBAAiB,IAAI,wBAAwB,EAC7C,mBAAmB,IAAI,0BAA0B,EACjD,uBAAuB,IAAI,8BAA8B,EACzD,cAAc,IAAI,qBAAqB,EACvC,mBAAmB,IAAI,0BAA0B,EACjD,aAAa,IAAI,oBAAoB,EACrC,oBAAoB,IAAI,2BAA2B,EACnD,kBAAkB,IAAI,yBAAyB,EAC/C,gBAAgB,IAAI,uBAAuB,EAC3C,gBAAgB,IAAI,uBAAuB,EAC3C,qBAAqB,IAAI,4BAA4B,GACtD,MAAM,mBAAmB,CAAC;AAE3B,YAAY,EACV,iBAAiB,EACjB,uBAAuB,EACvB,mBAAmB,EACnB,oBAAoB,EACpB,cAAc,EACd,mBAAmB,EACnB,8BAA8B,EAC9B,uBAAuB,EACvB,gBAAgB,EAChB,kBAAkB,EAClB,yBAAyB,EACzB,qBAAqB,EACrB,yBAAyB,EACzB,aAAa,EACb,gBAAgB,EAChB,qBAAqB,EACrB,mBAAmB,GACpB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACL,aAAa,EACb,kBAAkB,EAClB,kBAAkB,EAClB,sBAAsB,EACtB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,QAAQ,EACR,UAAU,EACV,WAAW,EACX,gBAAgB,EAChB,kBAAkB,EAClB,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,EACpB,uBAAuB,EACvB,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,kBAAkB,EAClB,OAAO,EACP,aAAa,EACb,cAAc,EACd,cAAc,EACd,WAAW,GACZ,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/sdk/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,YAAY,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAItD,OAAO,EAAE,aAAa,EAAE,QAAQ,IAAI,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAC/E,YAAY,EACV,oBAAoB,EACpB,IAAI,IAAI,WAAW,EACnB,OAAO,IAAI,cAAc,EACzB,gCAAgC,IAAI,uCAAuC,EAC3E,+BAA+B,IAAI,sCAAsC,EACzE,iCAAiC,IAAI,wCAAwC,EAC7E,WAAW,IAAI,kBAAkB,EACjC,eAAe,IAAI,sBAAsB,EACzC,eAAe,IAAI,sBAAsB,EACzC,aAAa,IAAI,oBAAoB,EACrC,iBAAiB,IAAI,wBAAwB,EAC7C,kBAAkB,IAAI,yBAAyB,EAC/C,+BAA+B,IAAI,sCAAsC,EACzE,4BAA4B,IAAI,mCAAmC,EACnE,4BAA4B,IAAI,mCAAmC,EACnE,2BAA2B,IAAI,kCAAkC,EACjE,6BAA6B,IAAI,oCAAoC,EACrE,sBAAsB,IAAI,6BAA6B,EACvD,iBAAiB,IAAI,wBAAwB,EAC7C,mBAAmB,IAAI,0BAA0B,EACjD,uBAAuB,IAAI,8BAA8B,EACzD,cAAc,IAAI,qBAAqB,EACvC,mBAAmB,IAAI,0BAA0B,EACjD,aAAa,IAAI,oBAAoB,EACrC,oBAAoB,IAAI,2BAA2B,EACnD,kBAAkB,IAAI,yBAAyB,EAC/C,gBAAgB,IAAI,uBAAuB,EAC3C,gBAAgB,IAAI,uBAAuB,EAC3C,qBAAqB,IAAI,4BAA4B,GACtD,MAAM,mBAAmB,CAAC;AAE3B,YAAY,EACV,iBAAiB,EACjB,uBAAuB,EACvB,mBAAmB,EACnB,oBAAoB,EACpB,cAAc,EACd,mBAAmB,EACnB,8BAA8B,EAC9B,uBAAuB,EACvB,gBAAgB,EAChB,kBAAkB,EAClB,yBAAyB,EACzB,qBAAqB,EACrB,yBAAyB,EACzB,aAAa,EACb,gBAAgB,EAChB,qBAAqB,EACrB,mBAAmB,GACpB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACL,aAAa,EACb,kBAAkB,EAClB,kBAAkB,EAClB,sBAAsB,EACtB,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,YAAY,CAAC;AACpB,YAAY,EACV,QAAQ,EACR,UAAU,EACV,WAAW,EACX,gBAAgB,EAChB,kBAAkB,EAClB,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,mBAAmB,EACnB,mBAAmB,EACnB,oBAAoB,EACpB,uBAAuB,EACvB,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,kBAAkB,EAClB,OAAO,EACP,aAAa,EACb,cAAc,EACd,cAAc,EACd,WAAW,GACZ,MAAM,YAAY,CAAC"}
package/dist/sdk/index.js CHANGED
@@ -980,6 +980,13 @@ class TodosV1Client {
980
980
  init
981
981
  });
982
982
  }
983
+ async failTask(id, body, init) {
984
+ return this.request("POST", `/v1/tasks/${encodeURIComponent(String(id))}/fail`, {
985
+ body,
986
+ query: undefined,
987
+ init
988
+ });
989
+ }
983
990
  async listTaskGitRefs(id, init) {
984
991
  return this.request("GET", `/v1/tasks/${encodeURIComponent(String(id))}/refs`, {
985
992
  body: undefined,
@@ -2,12 +2,13 @@ export interface Task {
2
2
  "id"?: string;
3
3
  "title"?: string;
4
4
  "description"?: string;
5
- "status"?: string;
6
- "priority"?: string;
5
+ "status"?: "pending" | "in_progress" | "completed" | "failed" | "cancelled";
6
+ "priority"?: "low" | "medium" | "high" | "critical";
7
7
  "project_id"?: string | null;
8
8
  "parent_id"?: string | null;
9
9
  "assigned_to"?: string | null;
10
10
  "agent_id"?: string | null;
11
+ "reason"?: string | null;
11
12
  "tags"?: Array<string>;
12
13
  "version"?: number;
13
14
  "created_at"?: string;
@@ -206,8 +207,8 @@ export interface CreateTemplateTaskInput {
206
207
  export interface CreateTaskInput {
207
208
  "title": string;
208
209
  "description"?: string | null;
209
- "status"?: string;
210
- "priority"?: string;
210
+ "status"?: "pending" | "in_progress" | "completed" | "failed" | "cancelled";
211
+ "priority"?: "low" | "medium" | "high" | "critical";
211
212
  "project_id"?: string;
212
213
  "parent_id"?: string;
213
214
  "plan_id"?: string;
@@ -218,8 +219,8 @@ export interface CreateTaskInput {
218
219
  export interface UpdateTaskInput {
219
220
  "title"?: string;
220
221
  "description"?: string;
221
- "status"?: string;
222
- "priority"?: string;
222
+ "status"?: "pending" | "in_progress" | "completed" | "failed" | "cancelled";
223
+ "priority"?: "low" | "medium" | "high" | "critical";
223
224
  "assigned_to"?: string;
224
225
  "project_id"?: string | null;
225
226
  "plan_id"?: string | null;
@@ -235,6 +236,15 @@ export interface CompleteTaskInput {
235
236
  "notes"?: string;
236
237
  "confidence"?: number;
237
238
  }
239
+ export interface FailTaskInput {
240
+ "agent_id"?: string;
241
+ "reason"?: string;
242
+ "retry"?: boolean;
243
+ }
244
+ export interface TaskFailureResult {
245
+ "task": Task;
246
+ "retryTask"?: Task;
247
+ }
238
248
  export interface CreateProjectInput {
239
249
  "name": string;
240
250
  "path": string;
@@ -822,6 +832,10 @@ export declare class TodosV1Client {
822
832
  completeTask(id: string, body?: CompleteTaskInput, init?: RequestInit): Promise<{
823
833
  "task"?: Task;
824
834
  }>;
835
+ /** Fail a task with an optional reason and retry copy */
836
+ failTask(id: string, body?: FailTaskInput, init?: RequestInit): Promise<{
837
+ "result": TaskFailureResult;
838
+ }>;
825
839
  /** List git branch and pull-request refs linked to a task */
826
840
  listTaskGitRefs(id: string, init?: RequestInit): Promise<{
827
841
  "refs": Array<TaskGitRef>;