@hasna/todos 0.15.11 → 0.15.13

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.11",
2126
+ version: "0.15.13",
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",
@@ -3025,7 +3025,6 @@ function responseTasks(value, expectedPlanId) {
3025
3025
  "reason",
3026
3026
  "spawned_from_session",
3027
3027
  "assigned_by",
3028
- "created_by",
3029
3028
  "assigned_from_project",
3030
3029
  "task_type",
3031
3030
  "delegated_from",
@@ -3034,6 +3033,9 @@ function responseTasks(value, expectedPlanId) {
3034
3033
  ]) {
3035
3034
  responseNullableString(task[field], `${label}.${field}`);
3036
3035
  }
3036
+ responseOptionalNullableString(task, "created_by", label);
3037
+ if (!("created_by" in task))
3038
+ task["created_by"] = null;
3037
3039
  responseStringArray(task.tags, `${label}.tags`);
3038
3040
  responseRecord(task.metadata, `${label}.metadata`);
3039
3041
  for (const field of [
@@ -5776,6 +5778,12 @@ function toListQuery(filter = {}) {
5776
5778
  query["offset"] = filter.offset;
5777
5779
  return query;
5778
5780
  }
5781
+ function priorityRank(priority) {
5782
+ return PRIORITY_RANK[priority ?? ""] ?? 4;
5783
+ }
5784
+ function compareCloudTaskOrder(a, b) {
5785
+ return priorityRank(a.priority) - priorityRank(b.priority) || b.created_at.localeCompare(a.created_at) || a.id.localeCompare(b.id);
5786
+ }
5779
5787
  async function fetchListTagsCapability(client) {
5780
5788
  const document = await requiredRemoteRoute(client, "/v1/openapi.json", () => client.transport.get("/openapi.json"));
5781
5789
  if (!document || typeof document !== "object" || Array.isArray(document))
@@ -5797,13 +5805,42 @@ async function requireTagsFilterCapability(client) {
5797
5805
  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");
5798
5806
  }
5799
5807
  }
5800
- async function cloudListTasks(client, filter = {}) {
5801
- if (filter.tags?.length)
5802
- await requireTagsFilterCapability(client);
5808
+ async function requestCloudTaskPage(client, filter) {
5803
5809
  const res = await requiredRemoteRoute(client, "/v1/tasks", () => client.list("tasks", { query: toListQuery(filter) }));
5804
5810
  const envelope = res.raw;
5805
5811
  return Array.isArray(envelope?.tasks) ? envelope.tasks : res.items;
5806
5812
  }
5813
+ async function cloudListTasks(client, filter = {}) {
5814
+ if (filter.tags?.length)
5815
+ await requireTagsFilterCapability(client);
5816
+ const statuses = Array.isArray(filter.status) ? filter.status : undefined;
5817
+ if (!statuses)
5818
+ return requestCloudTaskPage(client, filter);
5819
+ if (statuses.length === 0) {
5820
+ const { status: _status2, ...unfiltered } = filter;
5821
+ return requestCloudTaskPage(client, unfiltered);
5822
+ }
5823
+ if (statuses.length === 1) {
5824
+ return requestCloudTaskPage(client, { ...filter, status: statuses[0] });
5825
+ }
5826
+ const { status: _status, limit, offset, ...baseFilter } = filter;
5827
+ const start = offset ?? 0;
5828
+ const windowEnd = typeof limit === "number" ? start + limit : undefined;
5829
+ const pages = await Promise.all(statuses.map((status) => requestCloudTaskPage(client, {
5830
+ ...baseFilter,
5831
+ status,
5832
+ ...windowEnd === undefined ? {} : { limit: windowEnd }
5833
+ })));
5834
+ const seen = new Set;
5835
+ const union = pages.flat().filter((task) => {
5836
+ if (seen.has(task.id))
5837
+ return false;
5838
+ seen.add(task.id);
5839
+ return true;
5840
+ });
5841
+ union.sort(compareCloudTaskOrder);
5842
+ return union.slice(start, windowEnd);
5843
+ }
5807
5844
  async function cloudResolveTaskRef(client, ref) {
5808
5845
  const input = ref.trim().toLowerCase();
5809
5846
  if (!input)
@@ -5832,7 +5869,18 @@ async function cloudGetTask(client, id) {
5832
5869
  return raw == null ? null : unwrapTask(raw);
5833
5870
  }
5834
5871
  async function cloudCreateTask(client, input) {
5835
- return unwrapTask(await requiredRemoteRoute(client, "/v1/tasks", () => client.create("tasks", input)));
5872
+ const expectedParentId = typeof input["parent_id"] === "string" ? input["parent_id"] : null;
5873
+ const created = unwrapTask(await requiredRemoteRoute(client, "/v1/tasks", () => expectedParentId === null ? client.create("tasks", input) : client.create("tasks", input, { retry: false }), ["PARENT_TASK_NOT_FOUND"]));
5874
+ if (!created || typeof created.id !== "string" || !created.id.trim()) {
5875
+ 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");
5876
+ }
5877
+ if (expectedParentId === null)
5878
+ return created;
5879
+ const persisted = await cloudGetTask(client, created.id);
5880
+ if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== expectedParentId) {
5881
+ 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");
5882
+ }
5883
+ return persisted;
5836
5884
  }
5837
5885
  async function cloudUpdateTask(client, id, patch) {
5838
5886
  return unwrapTask(await client.update("tasks", id, patch));
@@ -6341,17 +6389,105 @@ async function cloudFindCommit(client, sha) {
6341
6389
  const env = raw ?? {};
6342
6390
  return env.commit ?? null;
6343
6391
  }
6392
+ function parseCloudTaskGitRef(value, route) {
6393
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
6394
+ throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} returned a non-object git ref; local SQLite fallback is disabled`);
6395
+ }
6396
+ const ref = value;
6397
+ const requiredStrings = ["id", "task_id", "name", "created_at", "updated_at"];
6398
+ if (requiredStrings.some((field) => typeof ref[field] !== "string" || !ref[field].trim())) {
6399
+ throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} returned an incomplete git ref identity; local SQLite fallback is disabled`);
6400
+ }
6401
+ if (ref["ref_type"] !== "branch" && ref["ref_type"] !== "pull_request") {
6402
+ throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} returned an invalid git ref type; local SQLite fallback is disabled`);
6403
+ }
6404
+ for (const field of ["url", "provider"]) {
6405
+ if (ref[field] !== null && typeof ref[field] !== "string") {
6406
+ throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} returned an invalid git ref ${field}; local SQLite fallback is disabled`);
6407
+ }
6408
+ }
6409
+ if (!ref["metadata"] || typeof ref["metadata"] !== "object" || Array.isArray(ref["metadata"])) {
6410
+ throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} returned invalid git ref metadata; local SQLite fallback is disabled`);
6411
+ }
6412
+ return ref;
6413
+ }
6414
+ function parseCloudTaskGitRefEnvelope(raw, route) {
6415
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
6416
+ throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} returned a non-object git ref envelope; local SQLite fallback is disabled`);
6417
+ }
6418
+ const envelope = raw;
6419
+ if (!Array.isArray(envelope["refs"]) || !Number.isSafeInteger(envelope["count"]) || envelope["count"] !== envelope["refs"].length) {
6420
+ throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} returned an incomplete git ref envelope; local SQLite fallback is disabled`);
6421
+ }
6422
+ return envelope["refs"].map((ref) => parseCloudTaskGitRef(ref, route));
6423
+ }
6424
+ function openApiHasOperation(document, path, method) {
6425
+ if (!document || typeof document !== "object" || Array.isArray(document))
6426
+ return false;
6427
+ const paths = document["paths"];
6428
+ if (!paths || typeof paths !== "object" || Array.isArray(paths))
6429
+ return false;
6430
+ const route = paths[path];
6431
+ return Boolean(route && typeof route === "object" && !Array.isArray(route) && route[method] && typeof route[method] === "object");
6432
+ }
6433
+ async function fetchGitRefCapabilities(client) {
6434
+ const document = await requiredRemoteRoute(client, "/v1/openapi.json", () => client.transport.get("/openapi.json"));
6435
+ const supported = new Set;
6436
+ if (openApiHasOperation(document, "/v1/tasks/{id}/refs", "get"))
6437
+ supported.add("task-read");
6438
+ if (openApiHasOperation(document, "/v1/tasks/{id}/refs", "post"))
6439
+ supported.add("task-write");
6440
+ if (openApiHasOperation(document, "/v1/refs/{ref}", "get"))
6441
+ supported.add("reverse-read");
6442
+ return supported;
6443
+ }
6444
+ async function requireGitRefCapabilities(client, required) {
6445
+ const authority = remoteAuthorityBase(client);
6446
+ let capabilities = gitRefCapabilityCache.get(authority);
6447
+ if (!capabilities) {
6448
+ capabilities = fetchGitRefCapabilities(client);
6449
+ gitRefCapabilityCache.set(authority, capabilities);
6450
+ }
6451
+ const supported = await capabilities;
6452
+ const missing = required.filter((capability) => !supported.has(capability));
6453
+ if (missing.length > 0) {
6454
+ 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");
6455
+ }
6456
+ }
6457
+ function sameCloudTaskGitRef(ref, expected) {
6458
+ return ref.id === expected.id && ref.task_id === expected.task_id && ref.ref_type === expected.ref_type && ref.name === expected.name;
6459
+ }
6460
+ async function cloudListTaskRefs(client, taskId) {
6461
+ await requireGitRefCapabilities(client, ["task-read"]);
6462
+ const route = `/v1/tasks/${encodeURIComponent(taskId)}/refs`;
6463
+ const raw = await requiredRemoteRoute(client, route, () => client.transport.get(`/tasks/${encodeURIComponent(taskId)}/refs`));
6464
+ return parseCloudTaskGitRefEnvelope(raw, route);
6465
+ }
6344
6466
  async function cloudLinkRef(client, taskId, input) {
6345
- const raw = await client.transport.post(`/tasks/${encodeURIComponent(taskId)}/refs`, input);
6346
- if (raw && typeof raw === "object" && "ref" in raw) {
6347
- return raw.ref;
6467
+ await requireGitRefCapabilities(client, ["task-read", "task-write", "reverse-read"]);
6468
+ const route = `/v1/tasks/${encodeURIComponent(taskId)}/refs`;
6469
+ const raw = await requiredRemoteRoute(client, route, () => client.transport.post(`/tasks/${encodeURIComponent(taskId)}/refs`, input));
6470
+ if (!raw || typeof raw !== "object" || Array.isArray(raw) || Object.keys(raw).length !== 1 || !("ref" in raw)) {
6471
+ throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} returned a non-authoritative git ref response envelope; ` + "local SQLite fallback is disabled");
6472
+ }
6473
+ const linked = parseCloudTaskGitRef(raw.ref, route);
6474
+ if (linked.task_id !== taskId || linked.ref_type !== input.ref_type || linked.name !== input.name) {
6475
+ throw new Error(`REMOTE_API_INCOMPATIBLE: ${route} returned a different git ref identity; local SQLite fallback is disabled`);
6476
+ }
6477
+ const [taskRefs, reverseRefs] = await Promise.all([
6478
+ cloudListTaskRefs(client, taskId),
6479
+ cloudFindRefs(client, input.name)
6480
+ ]);
6481
+ if (!taskRefs.some((ref) => sameCloudTaskGitRef(ref, linked)) || !reverseRefs.some((ref) => sameCloudTaskGitRef(ref, linked))) {
6482
+ 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");
6348
6483
  }
6349
- return raw;
6484
+ return linked;
6350
6485
  }
6351
6486
  async function cloudFindRefs(client, ref) {
6352
- const raw = await client.transport.get(`/refs/${encodeURIComponent(ref)}`);
6353
- const env = raw ?? {};
6354
- return Array.isArray(env.refs) ? env.refs : [];
6487
+ await requireGitRefCapabilities(client, ["reverse-read"]);
6488
+ const route = `/v1/refs/${encodeURIComponent(ref)}`;
6489
+ const raw = await requiredRemoteRoute(client, route, () => client.transport.get(`/refs/${encodeURIComponent(ref)}`));
6490
+ return parseCloudTaskGitRefEnvelope(raw, route);
6355
6491
  }
6356
6492
  async function cloudResolvePlan(client, ref, projectId) {
6357
6493
  const normalizedRef = ref.toLowerCase();
@@ -6509,9 +6645,6 @@ async function cloudRecordVerification(client, id, input) {
6509
6645
  }
6510
6646
  return raw;
6511
6647
  }
6512
- function priorityRank(priority) {
6513
- return PRIORITY_RANK[priority ?? ""] ?? 4;
6514
- }
6515
6648
  async function cloudActiveTasks(client, filter = {}) {
6516
6649
  const [pending, inProgress] = await Promise.all([
6517
6650
  cloudListTasks(client, { ...filter, status: "pending" }),
@@ -6825,7 +6958,7 @@ async function cloudTimeline(client, options = {}) {
6825
6958
  const limit = options.limit ?? 50;
6826
6959
  return { entries: entries.slice(offset, offset + limit), total, limit, offset };
6827
6960
  }
6828
- var UUID_RE, TRANSPORT_TOKENS, COMPLETION_EVIDENCE_FIELDS, completionCapabilityCache, SERVER_MODE_CANDIDATES, cachedServerMode = null, listTagsCapabilityCache, RELATION_HYDRATION_CONCURRENCY = 6, PRIORITY_RANK;
6961
+ var UUID_RE, TRANSPORT_TOKENS, COMPLETION_EVIDENCE_FIELDS, completionCapabilityCache, gitRefCapabilityCache, SERVER_MODE_CANDIDATES, cachedServerMode = null, PRIORITY_RANK, listTagsCapabilityCache, RELATION_HYDRATION_CONCURRENCY = 6;
6829
6962
  var init_cloud_router = __esm(() => {
6830
6963
  init_types();
6831
6964
  init_redaction();
@@ -6850,9 +6983,10 @@ var init_cloud_router = __esm(() => {
6850
6983
  "confidence"
6851
6984
  ];
6852
6985
  completionCapabilityCache = new Map;
6986
+ gitRefCapabilityCache = new Map;
6853
6987
  SERVER_MODE_CANDIDATES = ["postgres", "cloud", "self_hosted"];
6854
- listTagsCapabilityCache = new Map;
6855
6988
  PRIORITY_RANK = { critical: 0, high: 1, medium: 2, low: 3 };
6989
+ listTagsCapabilityCache = new Map;
6856
6990
  });
6857
6991
 
6858
6992
  // src/cli/stage-a.ts
@@ -12391,13 +12525,19 @@ function jsonModeRequested(argv = process.argv) {
12391
12525
  return argv.some((arg) => arg === "--json" || /^-[a-z]+$/i.test(arg) && arg.includes("j"));
12392
12526
  }
12393
12527
  function remoteErrorDetail(e) {
12394
- if (!e || typeof e !== "object")
12395
- return null;
12396
- const body = e.body;
12397
- if (!body || typeof body !== "object" || Array.isArray(body))
12398
- return null;
12399
- const detail = body.error;
12400
- return typeof detail === "string" && detail.trim() ? detail.trim() : null;
12528
+ const seen = new Set;
12529
+ let current = e;
12530
+ while (current && typeof current === "object" && !seen.has(current)) {
12531
+ seen.add(current);
12532
+ const body = current.body;
12533
+ if (body && typeof body === "object" && !Array.isArray(body)) {
12534
+ const detail = body.error;
12535
+ if (typeof detail === "string" && detail.trim())
12536
+ return detail.trim();
12537
+ }
12538
+ current = current.cause;
12539
+ }
12540
+ return null;
12401
12541
  }
12402
12542
  function handleError(e) {
12403
12543
  const baseMessage = e instanceof Error ? e.message : String(e);
@@ -21387,6 +21527,14 @@ async function cloudDetailRelations(cloud, id) {
21387
21527
  return { dependencies: [], blocked_by: [], blocks: [] };
21388
21528
  }
21389
21529
  }
21530
+ async function cloudDetailGitRefs(cloud, id) {
21531
+ try {
21532
+ return await cloudListTaskRefs(cloud, id);
21533
+ } catch (e) {
21534
+ console.error(chalk3.dim(`Warning: could not verify task git refs: ${e instanceof Error ? e.message : String(e)}`));
21535
+ return null;
21536
+ }
21537
+ }
21390
21538
  function resolveProjectIdOrSlug(input) {
21391
21539
  const db = getDatabase();
21392
21540
  if (isPathLike(input)) {
@@ -21959,7 +22107,8 @@ function registerTaskCommands(program2) {
21959
22107
  }
21960
22108
  const creatorFilterActive = Boolean(filter["created_by"] || filter["not_created_by"]);
21961
22109
  const requestedLimit = filter["limit"];
21962
- const reordersAfterQuery = Boolean(opts.sort);
22110
+ const combinesScalarStatusPages = Boolean(cloud && Array.isArray(filter["status"]) && filter["status"].length > 1);
22111
+ const reordersAfterQuery = Boolean(opts.sort) || combinesScalarStatusPages;
21963
22112
  const narrowsAfterQuery = Boolean(opts.dueToday) || Boolean(opts.overdue) || creatorFilterActive && cloud;
21964
22113
  const withholdLimit = requestedLimit !== undefined && (reordersAfterQuery || narrowsAfterQuery);
21965
22114
  const scanCeiling = cloud && (withholdLimit || requestedLimit === undefined) ? Math.max(requestedLimit ?? 0, listScanLimit()) : undefined;
@@ -22110,8 +22259,11 @@ function registerTaskCommands(program2) {
22110
22259
  let task2;
22111
22260
  if (cloud) {
22112
22261
  const remote = await cloudGetTask(cloud, await resolveTaskIdForCommand(id, cloud));
22113
- const commentPage = remote ? await cloudListComments(cloud, remote.id, page.request) : null;
22114
- const relations = remote ? await cloudDetailRelations(cloud, remote.id) : null;
22262
+ const [commentPage, relations, gitRefs] = remote ? await Promise.all([
22263
+ cloudListComments(cloud, remote.id, page.request),
22264
+ cloudDetailRelations(cloud, remote.id),
22265
+ cloudDetailGitRefs(cloud, remote.id)
22266
+ ]) : [null, null, null];
22115
22267
  task2 = remote ? {
22116
22268
  subtasks: [],
22117
22269
  ...remote,
@@ -22119,6 +22271,7 @@ function registerTaskCommands(program2) {
22119
22271
  dependencies: relations.dependencies,
22120
22272
  blocked_by: relations.blocked_by,
22121
22273
  blocks: relations.blocks,
22274
+ git_refs: gitRefs,
22122
22275
  comments: commentPage.comments,
22123
22276
  comments_page: {
22124
22277
  count: commentPage.count,
@@ -22131,6 +22284,8 @@ function registerTaskCommands(program2) {
22131
22284
  } else {
22132
22285
  const resolvedId = resolveTaskId(id);
22133
22286
  task2 = applyLocalCommentPage(getTaskWithRelations(resolvedId), page);
22287
+ const { getTaskGitRefs: getTaskGitRefs2 } = await Promise.resolve().then(() => (init_task_commits(), exports_task_commits));
22288
+ task2.git_refs = getTaskGitRefs2(resolvedId);
22134
22289
  }
22135
22290
  if (!task2) {
22136
22291
  handleError(new Error(`Task not found: ${id}`));
@@ -22243,8 +22398,11 @@ function registerTaskCommands(program2) {
22243
22398
  let task2;
22244
22399
  if (cloud) {
22245
22400
  const remote = await cloudGetTask(cloud, resolvedId);
22246
- const commentPage = remote ? await cloudListComments(cloud, remote.id, page.request) : null;
22247
- const relations = remote ? await cloudDetailRelations(cloud, remote.id) : null;
22401
+ const [commentPage, relations, gitRefs] = remote ? await Promise.all([
22402
+ cloudListComments(cloud, remote.id, page.request),
22403
+ cloudDetailRelations(cloud, remote.id),
22404
+ cloudDetailGitRefs(cloud, remote.id)
22405
+ ]) : [null, null, null];
22248
22406
  task2 = remote ? {
22249
22407
  subtasks: [],
22250
22408
  checklist: [],
@@ -22253,6 +22411,7 @@ function registerTaskCommands(program2) {
22253
22411
  dependencies: relations.dependencies,
22254
22412
  blocked_by: relations.blocked_by,
22255
22413
  blocks: relations.blocks,
22414
+ git_refs: gitRefs,
22256
22415
  comments: commentPage.comments,
22257
22416
  comments_page: {
22258
22417
  count: commentPage.count,
@@ -22270,7 +22429,7 @@ function registerTaskCommands(program2) {
22270
22429
  }
22271
22430
  if (globalOpts.json && !cloud) {
22272
22431
  const { listTaskFiles: listTaskFiles2 } = await Promise.resolve().then(() => (init_task_files(), exports_task_files));
22273
- const { getTaskCommits: getTaskCommits2 } = await Promise.resolve().then(() => (init_task_commits(), exports_task_commits));
22432
+ const { getTaskCommits: getTaskCommits2, getTaskGitRefs: getTaskGitRefs2 } = await Promise.resolve().then(() => (init_task_commits(), exports_task_commits));
22274
22433
  try {
22275
22434
  task2.files = listTaskFiles2(task2.id);
22276
22435
  } catch (e) {
@@ -22281,6 +22440,11 @@ function registerTaskCommands(program2) {
22281
22440
  } catch (e) {
22282
22441
  console.error(chalk3.dim(`Warning: could not load task commits: ${e instanceof Error ? e.message : String(e)}`));
22283
22442
  }
22443
+ try {
22444
+ task2.git_refs = getTaskGitRefs2(task2.id);
22445
+ } catch (e) {
22446
+ console.error(chalk3.dim(`Warning: could not load task git refs: ${e instanceof Error ? e.message : String(e)}`));
22447
+ }
22284
22448
  output(task2, true);
22285
22449
  return;
22286
22450
  }
@@ -28987,7 +29151,10 @@ function isResolvedTask(task) {
28987
29151
  return !(metadata && metadata["unresolved"] === true);
28988
29152
  }
28989
29153
  async function buildCloudProjectDependencyGraph(cloud, projectId) {
28990
- const tasks = await cloudListTasks(cloud, projectId ? { project_id: projectId } : {});
29154
+ const tasks = await cloudListTasks(cloud, {
29155
+ ...projectId ? { project_id: projectId } : {},
29156
+ include_subtasks: true
29157
+ });
28991
29158
  const edges = [];
28992
29159
  const seen = new Set;
28993
29160
  let cursor = 0;
@@ -29366,8 +29533,8 @@ function registerProjectCommands(program2) {
29366
29533
  const globalOpts = program2.opts();
29367
29534
  const cloud = getTodosCloudClient();
29368
29535
  if (!id) {
29369
- if (opts.needs || opts.remove || opts.graph || opts.direction !== "both") {
29370
- handleError(new Error("A task id is required with --needs, --remove, --graph, or --direction."));
29536
+ if (opts.needs || opts.remove || opts.direction !== "both") {
29537
+ handleError(new Error("A task id is required with --needs, --remove, or --direction."));
29371
29538
  }
29372
29539
  const projectRef = opts.project ?? globalOpts.project;
29373
29540
  if (cloud) {
@@ -34438,6 +34605,7 @@ class PostgresJsonRecordStore {
34438
34605
  const current = await this.get(type, value.id);
34439
34606
  if (current)
34440
34607
  return current;
34608
+ throw new Error(`POSTGRES_WRITE_PERSISTENCE_UNVERIFIED: ${type} ${value.id} was not accepted and no persisted readback exists`);
34441
34609
  }
34442
34610
  return value;
34443
34611
  }
@@ -35209,6 +35377,9 @@ class PostgresJsonRecordStore {
35209
35377
  }
35210
35378
  async function createTask2(input, store, context) {
35211
35379
  const timestamp2 = new Date().toISOString();
35380
+ if (input.parent_id && !await store.get("tasks", input.parent_id)) {
35381
+ throw new TaskNotFoundError(input.parent_id);
35382
+ }
35212
35383
  const linkedPlan = input.plan_id ? await store.get("plans", input.plan_id) : null;
35213
35384
  const requestedProjectId = input.project_id ?? context?.projectId ?? null;
35214
35385
  if (linkedPlan?.project_id && requestedProjectId && requestedProjectId !== linkedPlan.project_id) {
@@ -40717,6 +40888,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
40717
40888
  ProjectTaskListEnsureResult: projectTaskListEnsureResultSchema,
40718
40889
  ProjectTaskListRollbackResult: projectTaskListRollbackResultSchema,
40719
40890
  TaskComment: taskCommentSchema,
40891
+ TaskGitRef: taskGitRefSchema,
40720
40892
  Plan: planSchema,
40721
40893
  PlanProjectLinkReceipt: planProjectLinkReceiptSchema,
40722
40894
  PlanProjectLinkResult: planProjectLinkResultSchema,
@@ -40734,6 +40906,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
40734
40906
  status: { type: "string" },
40735
40907
  priority: { type: "string" },
40736
40908
  project_id: { type: "string" },
40909
+ parent_id: { type: "string" },
40737
40910
  plan_id: { type: "string" },
40738
40911
  assigned_to: { type: "string" },
40739
40912
  agent_id: { type: "string" },
@@ -41878,6 +42051,92 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
41878
42051
  }
41879
42052
  }
41880
42053
  },
42054
+ "/v1/tasks/{id}/refs": {
42055
+ get: {
42056
+ operationId: "listTaskGitRefs",
42057
+ summary: "List git branch and pull-request refs linked to a task",
42058
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
42059
+ responses: {
42060
+ "200": {
42061
+ content: {
42062
+ "application/json": {
42063
+ schema: {
42064
+ type: "object",
42065
+ additionalProperties: false,
42066
+ required: ["refs", "count"],
42067
+ properties: {
42068
+ refs: { type: "array", items: { $ref: "#/components/schemas/TaskGitRef" } },
42069
+ count: { type: "integer", minimum: 0 }
42070
+ }
42071
+ }
42072
+ }
42073
+ }
42074
+ }
42075
+ }
42076
+ },
42077
+ post: {
42078
+ operationId: "linkTaskGitRef",
42079
+ summary: "Link a git branch or pull-request ref to a task",
42080
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
42081
+ requestBody: {
42082
+ required: true,
42083
+ content: {
42084
+ "application/json": {
42085
+ schema: {
42086
+ type: "object",
42087
+ additionalProperties: false,
42088
+ required: ["ref_type", "name"],
42089
+ properties: {
42090
+ ref_type: { type: "string", enum: ["branch", "pull_request"] },
42091
+ name: { type: "string", minLength: 1 },
42092
+ url: { type: "string" },
42093
+ provider: { type: "string" },
42094
+ metadata: { type: "object", additionalProperties: true }
42095
+ }
42096
+ }
42097
+ }
42098
+ }
42099
+ },
42100
+ responses: {
42101
+ "201": {
42102
+ content: {
42103
+ "application/json": {
42104
+ schema: {
42105
+ type: "object",
42106
+ additionalProperties: false,
42107
+ required: ["ref"],
42108
+ properties: { ref: { $ref: "#/components/schemas/TaskGitRef" } }
42109
+ }
42110
+ }
42111
+ }
42112
+ }
42113
+ }
42114
+ }
42115
+ },
42116
+ "/v1/refs/{ref}": {
42117
+ get: {
42118
+ operationId: "findTaskGitRefs",
42119
+ summary: "Find task links by git branch or pull-request ref",
42120
+ parameters: [{ name: "ref", in: "path", required: true, schema: { type: "string" } }],
42121
+ responses: {
42122
+ "200": {
42123
+ content: {
42124
+ "application/json": {
42125
+ schema: {
42126
+ type: "object",
42127
+ additionalProperties: false,
42128
+ required: ["refs", "count"],
42129
+ properties: {
42130
+ refs: { type: "array", items: { $ref: "#/components/schemas/TaskGitRef" } },
42131
+ count: { type: "integer", minimum: 0 }
42132
+ }
42133
+ }
42134
+ }
42135
+ }
42136
+ }
42137
+ }
42138
+ }
42139
+ },
41881
42140
  "/v1/tasks/{id}/start": {
41882
42141
  post: {
41883
42142
  operationId: "startTask",
@@ -42329,7 +42588,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
42329
42588
  }
42330
42589
  };
42331
42590
  }
42332
- var taskSchema, projectSchema, taskListSchema, projectTaskListEnsureReceiptSchema, projectTaskListEnsureResultSchema, projectTaskListRollbackResultSchema, taskCommentSchema, planSchema, planProjectLinkReceiptSchema, planProjectLinkResultSchema, planProjectLinkRollbackResultSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
42591
+ var taskSchema, projectSchema, taskListSchema, projectTaskListEnsureReceiptSchema, projectTaskListEnsureResultSchema, projectTaskListRollbackResultSchema, taskCommentSchema, taskGitRefSchema, planSchema, planProjectLinkReceiptSchema, planProjectLinkResultSchema, planProjectLinkRollbackResultSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
42333
42592
  var init_openapi = __esm(() => {
42334
42593
  init_package_version();
42335
42594
  init_types();
@@ -42342,6 +42601,7 @@ var init_openapi = __esm(() => {
42342
42601
  status: { type: "string" },
42343
42602
  priority: { type: "string" },
42344
42603
  project_id: { type: "string", nullable: true },
42604
+ parent_id: { type: "string", nullable: true },
42345
42605
  assigned_to: { type: "string", nullable: true },
42346
42606
  agent_id: { type: "string", nullable: true },
42347
42607
  tags: { type: "array", items: { type: "string" } },
@@ -42465,6 +42725,32 @@ var init_openapi = __esm(() => {
42465
42725
  created_at: { type: "string", format: "date-time" }
42466
42726
  }
42467
42727
  };
42728
+ taskGitRefSchema = {
42729
+ type: "object",
42730
+ additionalProperties: false,
42731
+ required: [
42732
+ "id",
42733
+ "task_id",
42734
+ "ref_type",
42735
+ "name",
42736
+ "url",
42737
+ "provider",
42738
+ "metadata",
42739
+ "created_at",
42740
+ "updated_at"
42741
+ ],
42742
+ properties: {
42743
+ id: { type: "string", minLength: 1 },
42744
+ task_id: { type: "string", minLength: 1 },
42745
+ ref_type: { type: "string", enum: ["branch", "pull_request"] },
42746
+ name: { type: "string", minLength: 1 },
42747
+ url: { type: "string", nullable: true },
42748
+ provider: { type: "string", nullable: true },
42749
+ metadata: { type: "object", additionalProperties: true },
42750
+ created_at: { type: "string", format: "date-time" },
42751
+ updated_at: { type: "string", format: "date-time" }
42752
+ }
42753
+ };
42468
42754
  planSchema = {
42469
42755
  type: "object",
42470
42756
  required: ["id", "slug", "name", "status", "created_at", "updated_at"],
@@ -43210,8 +43496,25 @@ async function handleV1Request(req, url, dependencies = {}) {
43210
43496
  if (!body || typeof body.title !== "string" || !body.title.trim()) {
43211
43497
  return error(400, "title is required");
43212
43498
  }
43213
- const task = await store.tasks.create(body, contextFromPrincipal(principal, body));
43214
- return json4({ task }, 201);
43499
+ const storageContext = contextFromPrincipal(principal, body);
43500
+ if (body.parent_id !== undefined) {
43501
+ if (typeof body.parent_id !== "string" || !body.parent_id.trim()) {
43502
+ return error(400, "parent_id must be a non-empty task id", {
43503
+ code: "PARENT_TASK_ID_INVALID"
43504
+ });
43505
+ }
43506
+ if (!await store.tasks.get(body.parent_id, storageContext)) {
43507
+ return error(404, `parent task not found: ${body.parent_id}`, {
43508
+ code: "PARENT_TASK_NOT_FOUND"
43509
+ });
43510
+ }
43511
+ }
43512
+ const created = await store.tasks.create(body, storageContext);
43513
+ const persisted = created?.id ? await store.tasks.get(created.id, storageContext) : null;
43514
+ if (!persisted || persisted.id !== created.id || (persisted.parent_id ?? null) !== (body.parent_id ?? null)) {
43515
+ return error(500, "TASK_CREATE_PERSISTENCE_UNVERIFIED: task create was acknowledged but authoritative readback did not return the same stored task id and parent_id", { code: "TASK_CREATE_PERSISTENCE_UNVERIFIED" });
43516
+ }
43517
+ return json4({ task: persisted }, 201);
43215
43518
  }
43216
43519
  return error(405, `method ${method} not allowed on /v1/tasks`);
43217
43520
  }
@@ -43985,6 +44288,9 @@ async function handleV1Request(req, url, dependencies = {}) {
43985
44288
  candidate_task_ids: e.candidateTaskIds
43986
44289
  });
43987
44290
  }
44291
+ if (e instanceof TaskNotFoundError) {
44292
+ return error(404, e.message, { code: TaskNotFoundError.code });
44293
+ }
43988
44294
  if (e instanceof LockError)
43989
44295
  return error(409, e.message, { code: LockError.code });
43990
44296
  if (e instanceof TaskNotStartableError) {
package/dist/contracts.js CHANGED
@@ -12379,7 +12379,7 @@ var init_tasks = __esm(() => {
12379
12379
  // package.json
12380
12380
  var package_default = {
12381
12381
  name: "@hasna/todos",
12382
- version: "0.15.11",
12382
+ version: "0.15.13",
12383
12383
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
12384
12384
  type: "module",
12385
12385
  main: "dist/index.js",
package/dist/index.js CHANGED
@@ -12492,7 +12492,7 @@ var init_dispatches = __esm(() => {
12492
12492
  // package.json
12493
12493
  var package_default = {
12494
12494
  name: "@hasna/todos",
12495
- version: "0.15.11",
12495
+ version: "0.15.13",
12496
12496
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
12497
12497
  type: "module",
12498
12498
  main: "dist/index.js",
@@ -26973,6 +26973,7 @@ class PostgresJsonRecordStore {
26973
26973
  const current = await this.get(type, value.id);
26974
26974
  if (current)
26975
26975
  return current;
26976
+ throw new Error(`POSTGRES_WRITE_PERSISTENCE_UNVERIFIED: ${type} ${value.id} was not accepted and no persisted readback exists`);
26976
26977
  }
26977
26978
  return value;
26978
26979
  }
@@ -27744,6 +27745,9 @@ class PostgresJsonRecordStore {
27744
27745
  }
27745
27746
  async function createTask2(input, store, context) {
27746
27747
  const timestamp3 = new Date().toISOString();
27748
+ if (input.parent_id && !await store.get("tasks", input.parent_id)) {
27749
+ throw new TaskNotFoundError(input.parent_id);
27750
+ }
27747
27751
  const linkedPlan = input.plan_id ? await store.get("plans", input.plan_id) : null;
27748
27752
  const requestedProjectId = input.project_id ?? context?.projectId ?? null;
27749
27753
  if (linkedPlan?.project_id && requestedProjectId && requestedProjectId !== linkedPlan.project_id) {
@@ -1 +1 @@
1
- {"version":3,"file":"plan-project-link-contract.d.ts","sourceRoot":"","sources":["../../src/lib/plan-project-link-contract.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,IAAI,EACJ,sBAAsB,EACtB,qBAAqB,EACrB,6BAA6B,EAC7B,IAAI,EACL,MAAM,mBAAmB,CAAC;AAE3B,eAAO,MAAM,gCAAgC,EAAG,4BAAqC,CAAC;AAEtF,MAAM,MAAM,wBAAwB,GAChC,kCAAkC,GAClC,qCAAqC,GACrC,0CAA0C,GAC1C,6CAA6C,GAC7C,mCAAmC,GACnC,2CAA2C,GAC3C,wCAAwC,GACxC,qCAAqC,GACrC,gCAAgC,GAChC,qCAAqC,GACrC,+BAA+B,CAAC;AAEpC,qBAAa,oBAAqB,SAAQ,KAAK;IAE3C,QAAQ,CAAC,IAAI,EAAE,wBAAwB;IAEvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;gBAFhC,IAAI,EAAE,wBAAwB,EACvC,OAAO,EAAE,MAAM,EACN,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM;CAKjD;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAQnE;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAE5D;AAED,wBAAgB,sCAAsC,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CASxF;AAED,wBAAgB,wBAAwB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAEvE;AAED,wBAAgB,gCAAgC,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE1E;AAED,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAEpF;AAED,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,GAAG,YAAY,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,MAAM,CAQxG;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,sBAAsB,CAsBnF;AAED,KAAK,kCAAkC,GAAG;IACxC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF,KAAK,0CAA0C,GAAG;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AA8RF,wFAAwF;AACxF,wBAAgB,6BAA6B,CAC3C,KAAK,EAAE,OAAO,EACd,WAAW,EAAE,kCAAkC,GAC9C,qBAAqB,CA2CvB;AAED,sFAAsF;AACtF,wBAAgB,qCAAqC,CACnD,KAAK,EAAE,OAAO,EACd,WAAW,EAAE,0CAA0C,GACtD,6BAA6B,CA6B/B"}
1
+ {"version":3,"file":"plan-project-link-contract.d.ts","sourceRoot":"","sources":["../../src/lib/plan-project-link-contract.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,IAAI,EACJ,sBAAsB,EACtB,qBAAqB,EACrB,6BAA6B,EAC7B,IAAI,EACL,MAAM,mBAAmB,CAAC;AAE3B,eAAO,MAAM,gCAAgC,EAAG,4BAAqC,CAAC;AAEtF,MAAM,MAAM,wBAAwB,GAChC,kCAAkC,GAClC,qCAAqC,GACrC,0CAA0C,GAC1C,6CAA6C,GAC7C,mCAAmC,GACnC,2CAA2C,GAC3C,wCAAwC,GACxC,qCAAqC,GACrC,gCAAgC,GAChC,qCAAqC,GACrC,+BAA+B,CAAC;AAEpC,qBAAa,oBAAqB,SAAQ,KAAK;IAE3C,QAAQ,CAAC,IAAI,EAAE,wBAAwB;IAEvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;gBAFhC,IAAI,EAAE,wBAAwB,EACvC,OAAO,EAAE,MAAM,EACN,OAAO,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM;CAKjD;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAQnE;AAED,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAE5D;AAED,wBAAgB,sCAAsC,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CASxF;AAED,wBAAgB,wBAAwB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,CAEvE;AAED,wBAAgB,gCAAgC,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAE1E;AAED,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAEpF;AAED,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,GAAG,YAAY,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,MAAM,CAQxG;AAED,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,sBAAsB,CAsBnF;AAED,KAAK,kCAAkC,GAAG;IACxC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF,KAAK,0CAA0C,GAAG;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAgSF,wFAAwF;AACxF,wBAAgB,6BAA6B,CAC3C,KAAK,EAAE,OAAO,EACd,WAAW,EAAE,kCAAkC,GAC9C,qBAAqB,CA2CvB;AAED,sFAAsF;AACtF,wBAAgB,qCAAqC,CACnD,KAAK,EAAE,OAAO,EACd,WAAW,EAAE,0CAA0C,GACtD,6BAA6B,CA6B/B"}