@hasna/todos 0.11.93 → 0.11.95

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/cli/cloud-router.d.ts +12 -1
  2. package/dist/cli/cloud-router.d.ts.map +1 -1
  3. package/dist/cli/commands/plan-template-commands.d.ts.map +1 -1
  4. package/dist/cli/commands/task-commands.d.ts.map +1 -1
  5. package/dist/cli/index.js +885 -76
  6. package/dist/contracts.js +56 -50
  7. package/dist/db/storage-tombstones.d.ts +1 -1
  8. package/dist/db/storage-tombstones.d.ts.map +1 -1
  9. package/dist/db/templates.d.ts.map +1 -1
  10. package/dist/index.js +170 -56
  11. package/dist/lib/template-semantics.d.ts +8 -0
  12. package/dist/lib/template-semantics.d.ts.map +1 -0
  13. package/dist/mcp/index.js +605 -61
  14. package/dist/mcp/tools/task-crud.d.ts.map +1 -1
  15. package/dist/mcp/tools/task-meta-tools.d.ts.map +1 -1
  16. package/dist/mcp/tools/task-project-tools.d.ts.map +1 -1
  17. package/dist/mcp.js +1 -1
  18. package/dist/registry.js +56 -50
  19. package/dist/release-provenance.json +5 -5
  20. package/dist/sdk/index.js +35 -0
  21. package/dist/sdk/v1.generated.d.ts +105 -1
  22. package/dist/sdk/v1.generated.d.ts.map +1 -1
  23. package/dist/server/index.js +611 -67
  24. package/dist/server/openapi.d.ts +496 -0
  25. package/dist/server/openapi.d.ts.map +1 -1
  26. package/dist/server/v1.d.ts.map +1 -1
  27. package/dist/storage/interfaces.d.ts +3 -2
  28. package/dist/storage/interfaces.d.ts.map +1 -1
  29. package/dist/storage/postgres-adapter.d.ts.map +1 -1
  30. package/dist/storage/postgres-sync.d.ts +1 -1
  31. package/dist/storage/postgres-sync.d.ts.map +1 -1
  32. package/dist/storage/shadow-outbox.d.ts.map +1 -1
  33. package/dist/storage/shadow.d.ts +1 -1
  34. package/dist/storage/shadow.d.ts.map +1 -1
  35. package/dist/storage/sqlite-snapshot.d.ts.map +1 -1
  36. package/dist/storage.js +169 -55
  37. package/package.json +1 -1
package/dist/mcp/index.js CHANGED
@@ -11043,6 +11043,50 @@ var init_recurrence = __esm(() => {
11043
11043
  };
11044
11044
  });
11045
11045
 
11046
+ // src/lib/template-semantics.ts
11047
+ function resolveTemplateVariables(templateVars, provided) {
11048
+ const merged = { ...provided };
11049
+ for (const variable of templateVars) {
11050
+ if (merged[variable.name] === undefined && variable.default !== undefined) {
11051
+ merged[variable.name] = variable.default;
11052
+ }
11053
+ }
11054
+ const missing = templateVars.filter((variable) => variable.required && merged[variable.name] === undefined).map((variable) => variable.name);
11055
+ if (missing.length > 0) {
11056
+ throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
11057
+ }
11058
+ return merged;
11059
+ }
11060
+ function substituteTemplateVariables(text, variables) {
11061
+ let result = text;
11062
+ for (const [key, value] of Object.entries(variables)) {
11063
+ result = result.replace(new RegExp(`\\{${key}\\}`, "g"), value);
11064
+ }
11065
+ return result;
11066
+ }
11067
+ function evaluateTemplateCondition(condition, variables) {
11068
+ if (!condition || condition.trim() === "")
11069
+ return true;
11070
+ const trimmed = condition.trim();
11071
+ const equal = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
11072
+ if (equal)
11073
+ return (variables[equal[1]] ?? "") === equal[2].trim();
11074
+ const unequal = trimmed.match(/^\{([^}]+)\}\s*!=\s*(.+)$/);
11075
+ if (unequal)
11076
+ return (variables[unequal[1]] ?? "") !== unequal[2].trim();
11077
+ const falsy = trimmed.match(/^!\{([^}]+)\}$/);
11078
+ if (falsy) {
11079
+ const value = variables[falsy[1]];
11080
+ return !value || value === "" || value === "false";
11081
+ }
11082
+ const truthy = trimmed.match(/^\{([^}]+)\}$/);
11083
+ if (truthy) {
11084
+ const value = variables[truthy[1]];
11085
+ return !!value && value !== "" && value !== "false";
11086
+ }
11087
+ return true;
11088
+ }
11089
+
11046
11090
  // src/db/templates.ts
11047
11091
  var exports_templates = {};
11048
11092
  __export(exports_templates, {
@@ -11138,6 +11182,14 @@ function deleteTemplate(id, db) {
11138
11182
  payload: template,
11139
11183
  version: template.version
11140
11184
  }, d);
11185
+ for (const task of getTemplateTasks(resolved, d)) {
11186
+ recordStorageTombstone({
11187
+ object_type: "template_tasks",
11188
+ object_id: task.id,
11189
+ payload: task,
11190
+ version: 1
11191
+ }, d);
11192
+ }
11141
11193
  return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
11142
11194
  }
11143
11195
  function updateTemplate(id, updates, db) {
@@ -11269,34 +11321,7 @@ function getTemplateTasks(templateId, db) {
11269
11321
  return rows.map(rowToTemplateTask);
11270
11322
  }
11271
11323
  function evaluateCondition(condition, variables) {
11272
- if (!condition || condition.trim() === "")
11273
- return true;
11274
- const trimmed = condition.trim();
11275
- const eqMatch = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
11276
- if (eqMatch) {
11277
- const varName = eqMatch[1];
11278
- const expected = eqMatch[2].trim();
11279
- return (variables[varName] ?? "") === expected;
11280
- }
11281
- const neqMatch = trimmed.match(/^\{([^}]+)\}\s*!=\s*(.+)$/);
11282
- if (neqMatch) {
11283
- const varName = neqMatch[1];
11284
- const expected = neqMatch[2].trim();
11285
- return (variables[varName] ?? "") !== expected;
11286
- }
11287
- const falsyMatch = trimmed.match(/^!\{([^}]+)\}$/);
11288
- if (falsyMatch) {
11289
- const varName = falsyMatch[1];
11290
- const val = variables[varName];
11291
- return !val || val === "" || val === "false";
11292
- }
11293
- const truthyMatch = trimmed.match(/^\{([^}]+)\}$/);
11294
- if (truthyMatch) {
11295
- const varName = truthyMatch[1];
11296
- const val = variables[varName];
11297
- return !!val && val !== "" && val !== "false";
11298
- }
11299
- return true;
11324
+ return evaluateTemplateCondition(condition, variables);
11300
11325
  }
11301
11326
  function exportTemplate(id, db) {
11302
11327
  const d = db || getDatabase();
@@ -11369,29 +11394,10 @@ function listTemplateVersions(id, db) {
11369
11394
  return d.query("SELECT * FROM template_versions WHERE template_id = ? ORDER BY version DESC").all(resolved);
11370
11395
  }
11371
11396
  function resolveVariables(templateVars, provided) {
11372
- const merged = { ...provided };
11373
- for (const v of templateVars) {
11374
- if (merged[v.name] === undefined && v.default !== undefined) {
11375
- merged[v.name] = v.default;
11376
- }
11377
- }
11378
- const missing = [];
11379
- for (const v of templateVars) {
11380
- if (v.required && merged[v.name] === undefined) {
11381
- missing.push(v.name);
11382
- }
11383
- }
11384
- if (missing.length > 0) {
11385
- throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
11386
- }
11387
- return merged;
11397
+ return resolveTemplateVariables(templateVars, provided);
11388
11398
  }
11389
11399
  function substituteVars(text, variables) {
11390
- let result = text;
11391
- for (const [key, val] of Object.entries(variables)) {
11392
- result = result.replace(new RegExp(`\\{${key}\\}`, "g"), val);
11393
- }
11394
- return result;
11400
+ return substituteTemplateVariables(text, variables);
11395
11401
  }
11396
11402
  function tasksFromTemplate(templateId, projectId, variables, taskListId, db, _visitedTemplateIds) {
11397
11403
  const d = db || getDatabase();
@@ -16658,6 +16664,7 @@ var init_token_utils = __esm(() => {
16658
16664
 
16659
16665
  // src/cli/cloud-router.ts
16660
16666
  import { resolveStorageClient } from "@hasna/contracts/client/storage";
16667
+ import { resolve as resolvePath } from "path";
16661
16668
  function cleanMode(value) {
16662
16669
  const normalized = value?.trim().toLowerCase();
16663
16670
  return normalized || null;
@@ -16940,6 +16947,39 @@ async function cloudListProjects(client) {
16940
16947
  const envelope = res.raw;
16941
16948
  return Array.isArray(envelope?.projects) ? envelope.projects : res.items;
16942
16949
  }
16950
+ function cloudProjectSlug(value) {
16951
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
16952
+ }
16953
+ function cloudProjectPathBasename(value) {
16954
+ return value.replace(/[\\/]+$/, "").split(/[\\/]/).pop() ?? value;
16955
+ }
16956
+ function uniqueProjectMatches(projects, predicate) {
16957
+ return [...new Map(projects.filter(predicate).map((project) => [project.id, project])).values()];
16958
+ }
16959
+ function resolveCloudProjectRef(projects, ref) {
16960
+ const input = ref.trim();
16961
+ const normalizedRef = input.toLowerCase();
16962
+ const pathLike = input.startsWith(".") || input.includes("/") || input.includes("\\");
16963
+ const normalizedPath = pathLike ? resolvePath(input) : undefined;
16964
+ const slug = cloudProjectSlug(pathLike ? cloudProjectPathBasename(input) : input);
16965
+ const matchGroups = [
16966
+ uniqueProjectMatches(projects, (project) => project.id.toLowerCase() === normalizedRef),
16967
+ uniqueProjectMatches(projects, (project) => project.path === input || normalizedPath !== undefined && resolvePath(project.path) === normalizedPath),
16968
+ uniqueProjectMatches(projects, (project) => project.name.toLowerCase() === normalizedRef),
16969
+ uniqueProjectMatches(projects, (project) => project.task_list_id === input || cloudProjectSlug(project.name) === slug || cloudProjectSlug(cloudProjectPathBasename(project.path)) === slug),
16970
+ uniqueProjectMatches(projects, (project) => project.id.toLowerCase().startsWith(normalizedRef))
16971
+ ];
16972
+ for (const matches of matchGroups) {
16973
+ if (matches.length === 1)
16974
+ return matches[0].id;
16975
+ if (matches.length > 1)
16976
+ throw new Error(`Project reference is ambiguous: "${input}"`);
16977
+ }
16978
+ throw new Error(`Project not found: "${input}"`);
16979
+ }
16980
+ async function cloudResolveProjectRef(client, ref) {
16981
+ return resolveCloudProjectRef(await cloudListProjects(client), ref);
16982
+ }
16943
16983
  async function cloudAddComment(client, taskId, input) {
16944
16984
  const raw = await client.transport.post(`/tasks/${encodeURIComponent(taskId)}/comments`, input);
16945
16985
  const comment = raw && typeof raw === "object" && "comment" in raw ? raw.comment : raw;
@@ -16984,9 +17024,46 @@ async function cloudReleaseAgent(client, idOrName, sessionId) {
16984
17024
  const env = raw ?? {};
16985
17025
  return { agent: env.agent ?? null, released: env.released !== false };
16986
17026
  }
16987
- var CLOUD_MODES, VALID_STORAGE_MODES, completionCapabilityCache;
17027
+ async function cloudListTaskLists(client, projectId) {
17028
+ const query = projectId ? { project_id: projectId } : {};
17029
+ const raw = await requiredRemoteRoute(client, "/v1/task-lists", () => client.transport.get("/task-lists", { query }));
17030
+ const envelope = raw ?? {};
17031
+ if (Array.isArray(envelope.task_lists))
17032
+ return envelope.task_lists;
17033
+ if (Array.isArray(envelope.taskLists))
17034
+ return envelope.taskLists;
17035
+ return Array.isArray(raw) ? raw : [];
17036
+ }
17037
+ async function cloudResolveTaskListRef(client, ref, projectId) {
17038
+ const input = ref.trim();
17039
+ const normalizedIdRef = input.toLowerCase();
17040
+ if (UUID_RE.test(input) && !projectId)
17041
+ return normalizedIdRef;
17042
+ const lists = await cloudListTaskLists(client, projectId);
17043
+ const exactIds = lists.filter((list) => list.id.toLowerCase() === normalizedIdRef);
17044
+ if (exactIds.length === 1)
17045
+ return exactIds[0].id;
17046
+ if (exactIds.length > 1) {
17047
+ throw new Error(`Task list reference is ambiguous: "${input}"`);
17048
+ }
17049
+ const slugs = lists.filter((list) => list.slug === input);
17050
+ if (slugs.length === 1)
17051
+ return slugs[0].id;
17052
+ if (slugs.length > 1) {
17053
+ throw new Error(`Task list reference is ambiguous: "${input}"`);
17054
+ }
17055
+ const prefixes = lists.filter((list) => list.id.toLowerCase().startsWith(normalizedIdRef));
17056
+ if (prefixes.length === 1)
17057
+ return prefixes[0].id;
17058
+ if (prefixes.length > 1) {
17059
+ throw new Error(`Task list reference is ambiguous: "${input}"`);
17060
+ }
17061
+ throw new Error(`Task list not found: "${input}"`);
17062
+ }
17063
+ var UUID_RE, CLOUD_MODES, VALID_STORAGE_MODES, completionCapabilityCache;
16988
17064
  var init_cloud_router = __esm(() => {
16989
17065
  init_redaction();
17066
+ UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
16990
17067
  CLOUD_MODES = new Set(["self_hosted", "cloud", "remote", "hybrid"]);
16991
17068
  VALID_STORAGE_MODES = new Set(["local", ...CLOUD_MODES]);
16992
17069
  completionCapabilityCache = new Map;
@@ -17273,6 +17350,17 @@ ${task.description}` : null
17273
17350
  patch.estimated_minutes = estimate;
17274
17351
  if (deadline !== undefined)
17275
17352
  patch.due_at = deadline;
17353
+ if (typeof patch.project_id === "string" && patch.project_id) {
17354
+ patch.project_id = await cloudResolveProjectRef(cloud, patch.project_id);
17355
+ }
17356
+ if (typeof patch.task_list_id === "string" && patch.task_list_id) {
17357
+ let scope = typeof patch.project_id === "string" ? patch.project_id : undefined;
17358
+ if (!scope) {
17359
+ const current = await cloudGetTask(cloud, task_id2);
17360
+ scope = current?.project_id ?? undefined;
17361
+ }
17362
+ patch.task_list_id = await cloudResolveTaskListRef(cloud, patch.task_list_id, scope);
17363
+ }
17276
17364
  if (version2 !== undefined)
17277
17365
  patch.version = version2;
17278
17366
  const updated = await cloudUpdateTask(cloud, task_id2, patch);
@@ -23286,6 +23374,63 @@ function registerTaskProjectTools(server, ctx) {
23286
23374
  }
23287
23375
  });
23288
23376
  }
23377
+ if (shouldRegisterTool("move_task")) {
23378
+ server.tool("move_task", "Re-parent a task to another project and/or task list, preserving its id and history. " + "A task list is project-scoped, so moving to a new project detaches the old list unless to_list is given.", {
23379
+ task_id: exports_external.string().describe("Task ID"),
23380
+ to_project: exports_external.string().optional().describe("Destination project ID, slug, or path"),
23381
+ to_list: exports_external.string().optional().describe("Destination task list (UUID, or slug resolved in the destination project)"),
23382
+ clear_list: exports_external.boolean().optional().describe("Detach from its task list (set task_list_id to null)"),
23383
+ version: exports_external.number().optional().describe("Expected version for optimistic locking")
23384
+ }, async ({ task_id, to_project, to_list, clear_list, version }) => {
23385
+ try {
23386
+ if (!to_project && !to_list && !clear_list) {
23387
+ throw new Error("Nothing to move: pass to_project, to_list, or clear_list.");
23388
+ }
23389
+ if (to_list && clear_list) {
23390
+ throw new Error("Use either to_list or clear_list, not both.");
23391
+ }
23392
+ const cloud = getTodosCloudClient();
23393
+ if (cloud) {
23394
+ const current2 = await cloudGetTask(cloud, task_id);
23395
+ if (!current2)
23396
+ throw new Error(`Task not found: ${task_id}`);
23397
+ const targetProjectId2 = to_project ? await cloudResolveProjectRef(cloud, to_project) : undefined;
23398
+ const scope = targetProjectId2 ?? current2.project_id ?? undefined;
23399
+ const patch = {};
23400
+ if (targetProjectId2 !== undefined)
23401
+ patch.project_id = targetProjectId2;
23402
+ if (to_list)
23403
+ patch.task_list_id = await cloudResolveTaskListRef(cloud, to_list, scope);
23404
+ else if (clear_list)
23405
+ patch.task_list_id = null;
23406
+ else if (targetProjectId2 && targetProjectId2 !== current2.project_id)
23407
+ patch.task_list_id = null;
23408
+ if (version !== undefined)
23409
+ patch.version = version;
23410
+ const task2 = await cloudUpdateTask(cloud, task_id, patch);
23411
+ return { content: [{ type: "text", text: formatTask(task2) }] };
23412
+ }
23413
+ const resolvedId = resolveId(task_id);
23414
+ const current = getTask(resolvedId);
23415
+ if (!current)
23416
+ throw new Error(`Task not found: ${task_id}`);
23417
+ const targetProjectId = to_project ? resolveId(to_project, "projects") : undefined;
23418
+ const updates = {};
23419
+ if (targetProjectId !== undefined)
23420
+ updates.project_id = targetProjectId;
23421
+ if (to_list)
23422
+ updates.task_list_id = resolveId(to_list, "task_lists");
23423
+ else if (clear_list)
23424
+ updates.task_list_id = null;
23425
+ else if (targetProjectId && targetProjectId !== current.project_id)
23426
+ updates.task_list_id = null;
23427
+ const task = updateWithOptionalVersion(resolvedId, updates, version);
23428
+ return { content: [{ type: "text", text: formatTask(task) }] };
23429
+ } catch (e) {
23430
+ return { content: [{ type: "text", text: formatError(e) }], isError: true };
23431
+ }
23432
+ });
23433
+ }
23289
23434
  if (shouldRegisterTool("reschedule_task")) {
23290
23435
  server.tool("reschedule_task", "Update a task's deadline.", {
23291
23436
  task_id: exports_external.string().describe("Task ID"),
@@ -27608,6 +27753,7 @@ function registerTaskMetaTools(server, ctx) {
27608
27753
  complete_task: "complete_task \u2014 Mark task completed. Params: task_id, confidence, completed_at, version",
27609
27754
  cancel_task: "cancel_task \u2014 Cancel a task. Params: task_id, version",
27610
27755
  reassign_task: "reassign_task \u2014 Change task assignee. Params: task_id, new_assignee, version",
27756
+ move_task: "move_task \u2014 Re-parent a task to another project and/or task list (keeps its id and history). Params: task_id (required), to_project, to_list, clear_list, version",
27611
27757
  reschedule_task: "reschedule_task \u2014 Update deadline. Params: task_id, deadline, version",
27612
27758
  prioritize_task: "prioritize_task \u2014 Set priority. Params: task_id, priority, version",
27613
27759
  search_tasks: "search_tasks \u2014 Full-text search. Params: query, project_id, status, limit",
@@ -30647,7 +30793,7 @@ var package_default;
30647
30793
  var init_package = __esm(() => {
30648
30794
  package_default = {
30649
30795
  name: "@hasna/todos",
30650
- version: "0.11.93",
30796
+ version: "0.11.95",
30651
30797
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
30652
30798
  type: "module",
30653
30799
  main: "dist/index.js",
@@ -39991,6 +40137,7 @@ function exportSqliteTodosStorageSnapshot(db) {
39991
40137
  agents: listAgents({ include_archived: true }, d),
39992
40138
  taskLists: listTaskLists(undefined, d),
39993
40139
  templates: listTemplates(d),
40140
+ templateTasks: listTemplates(d).flatMap((template) => getTemplateTasks(template.id, d)),
39994
40141
  auditHistory: getRecentActivity(Number.MAX_SAFE_INTEGER, d),
39995
40142
  tombstones: listStorageTombstones(d)
39996
40143
  };
@@ -40040,6 +40187,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
40040
40187
  applyRows("task_lists", "task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
40041
40188
  applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at");
40042
40189
  applyRows("templates", "task_templates", TEMPLATE_COLUMNS, snapshot.templates);
40190
+ applyRows("template_tasks", "template_tasks", TEMPLATE_TASK_COLUMNS, snapshot.templateTasks ?? []);
40043
40191
  applyRows("tasks", "tasks", TASK_COLUMNS, sortedTasks2(snapshot.tasks), "updated_at", (row, changed) => {
40044
40192
  if (changed && Array.isArray(row["tags"]) && typeof row["id"] === "string") {
40045
40193
  replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
@@ -40144,6 +40292,8 @@ function tableForTombstone(objectType2) {
40144
40292
  return "task_lists";
40145
40293
  if (objectType2 === "templates")
40146
40294
  return "task_templates";
40295
+ if (objectType2 === "template_tasks")
40296
+ return "template_tasks";
40147
40297
  return "task_history";
40148
40298
  }
40149
40299
  function listRows(db, table, columns) {
@@ -40180,7 +40330,7 @@ function clockColumnsForTable(table) {
40180
40330
  return ["created_at"];
40181
40331
  return ["updated_at", "created_at"];
40182
40332
  }
40183
- var PROJECT_COLUMNS, PROJECT_MACHINE_PATH_COLUMNS, TASK_LIST_COLUMNS, PLAN_COLUMNS, AGENT_COLUMNS, TEMPLATE_COLUMNS, TASK_COLUMNS, AUDIT_COLUMNS, JSON_COLUMNS, BOOLEAN_COLUMNS;
40333
+ var PROJECT_COLUMNS, PROJECT_MACHINE_PATH_COLUMNS, TASK_LIST_COLUMNS, PLAN_COLUMNS, AGENT_COLUMNS, TEMPLATE_COLUMNS, TEMPLATE_TASK_COLUMNS, TASK_COLUMNS, AUDIT_COLUMNS, JSON_COLUMNS, BOOLEAN_COLUMNS;
40184
40334
  var init_sqlite_snapshot = __esm(() => {
40185
40335
  init_database();
40186
40336
  init_agents();
@@ -40274,6 +40424,21 @@ var init_sqlite_snapshot = __esm(() => {
40274
40424
  "machine_id",
40275
40425
  "synced_at"
40276
40426
  ];
40427
+ TEMPLATE_TASK_COLUMNS = [
40428
+ "id",
40429
+ "template_id",
40430
+ "position",
40431
+ "title_pattern",
40432
+ "description",
40433
+ "priority",
40434
+ "tags",
40435
+ "task_type",
40436
+ "condition",
40437
+ "include_template_id",
40438
+ "depends_on_positions",
40439
+ "metadata",
40440
+ "created_at"
40441
+ ];
40277
40442
  TASK_COLUMNS = [
40278
40443
  "id",
40279
40444
  "short_id",
@@ -40342,7 +40507,7 @@ var init_sqlite_snapshot = __esm(() => {
40342
40507
  "created_at",
40343
40508
  "machine_id"
40344
40509
  ];
40345
- JSON_COLUMNS = new Set(["tags", "metadata", "permissions", "capabilities", "variables"]);
40510
+ JSON_COLUMNS = new Set(["tags", "metadata", "permissions", "capabilities", "variables", "depends_on_positions"]);
40346
40511
  BOOLEAN_COLUMNS = new Set(["requires_approval"]);
40347
40512
  });
40348
40513
 
@@ -40732,6 +40897,7 @@ function snapshotEntries(snapshot) {
40732
40897
  ...snapshot.agents.map((payload) => entry("agents", payload, snapshot.exportedAt)),
40733
40898
  ...snapshot.taskLists.map((payload) => entry("task_lists", payload, snapshot.exportedAt)),
40734
40899
  ...snapshot.templates.map((payload) => entry("templates", payload, snapshot.exportedAt)),
40900
+ ...(snapshot.templateTasks ?? []).map((payload) => entry("template_tasks", payload, snapshot.exportedAt)),
40735
40901
  ...snapshot.auditHistory.map((payload) => entry("audit_history", payload, snapshot.exportedAt)),
40736
40902
  ...(snapshot.tombstones ?? []).map((tombstone) => ({
40737
40903
  type: tombstone.object_type,
@@ -40783,6 +40949,7 @@ function rowsToSnapshot(rows) {
40783
40949
  agents: [],
40784
40950
  taskLists: [],
40785
40951
  templates: [],
40952
+ templateTasks: [],
40786
40953
  auditHistory: [],
40787
40954
  tombstones: []
40788
40955
  };
@@ -40817,6 +40984,8 @@ function rowsToSnapshot(rows) {
40817
40984
  snapshot.taskLists.push(payload);
40818
40985
  else if (row.object_type === "templates")
40819
40986
  snapshot.templates.push(payload);
40987
+ else if (row.object_type === "template_tasks")
40988
+ snapshot.templateTasks.push(payload);
40820
40989
  else if (row.object_type === "audit_history")
40821
40990
  snapshot.auditHistory.push(payload);
40822
40991
  }
@@ -41034,6 +41203,9 @@ class TodosShadowOutbox {
41034
41203
  case "templates":
41035
41204
  snapshot.templates.push(record);
41036
41205
  break;
41206
+ case "template_tasks":
41207
+ snapshot.templateTasks.push(record);
41208
+ break;
41037
41209
  case "audit_history":
41038
41210
  snapshot.auditHistory.push(record);
41039
41211
  break;
@@ -41100,6 +41272,7 @@ function emptySnapshot() {
41100
41272
  agents: [],
41101
41273
  taskLists: [],
41102
41274
  templates: [],
41275
+ templateTasks: [],
41103
41276
  auditHistory: [],
41104
41277
  tombstones: []
41105
41278
  };
@@ -42299,10 +42472,13 @@ function createPostgresTodosStorageAdapter(options) {
42299
42472
  get: (id) => store.get("templates", id),
42300
42473
  list: async () => (await store.list("templates")).sort((a, b) => a.name.localeCompare(b.name)),
42301
42474
  update: (id, input) => updateTemplate2(id, input, store),
42302
- delete: (id, context) => store.delete("templates", id, context),
42475
+ delete: (id, context) => deleteTemplate2(id, store, context),
42303
42476
  getWithTasks: async (id) => {
42304
42477
  const template = await store.get("templates", id);
42305
- return template ? { ...template, tasks: [] } : null;
42478
+ if (!template)
42479
+ return null;
42480
+ const tasks = (await store.list("template_tasks")).filter((task2) => task2.template_id === id).sort((left, right) => left.position - right.position || left.id.localeCompare(right.id));
42481
+ return { ...template, tasks };
42306
42482
  }
42307
42483
  },
42308
42484
  audit: {
@@ -42590,6 +42766,57 @@ class PostgresJsonRecordStore {
42590
42766
  }
42591
42767
  return value;
42592
42768
  }
42769
+ async createTemplateWithTasks(template, tasks, context = {}) {
42770
+ await this.ensureSchema();
42771
+ const records = [
42772
+ { object_type: "templates", object_id: template.id, payload: template, updated_at: template.created_at, version: template.version },
42773
+ ...tasks.map((task2) => ({ object_type: "template_tasks", object_id: task2.id, payload: task2, updated_at: task2.created_at, version: 1 }))
42774
+ ];
42775
+ const result = await this.options.client.query(`/* todos:create-template-with-tasks-atomic */ WITH input AS (
42776
+ SELECT value->>'object_type' AS object_type,
42777
+ value->>'object_id' AS object_id,
42778
+ value->'payload' AS payload,
42779
+ value->>'updated_at' AS updated_at,
42780
+ COALESCE((value->>'version')::integer, 1) AS version
42781
+ FROM jsonb_array_elements($2::jsonb) AS value
42782
+ ) INSERT INTO ${this.tableName} (
42783
+ service, object_type, object_id, payload, updated_at,
42784
+ deleted_at, source_machine_id, version
42785
+ ) SELECT $1, object_type, object_id, payload, updated_at::timestamptz,
42786
+ NULL, $3, version
42787
+ FROM input
42788
+ ON CONFLICT (service, object_type, object_id) DO UPDATE SET
42789
+ payload = EXCLUDED.payload,
42790
+ updated_at = EXCLUDED.updated_at,
42791
+ deleted_at = NULL,
42792
+ source_machine_id = EXCLUDED.source_machine_id,
42793
+ version = EXCLUDED.version
42794
+ WHERE ${this.tableName}.updated_at IS NULL
42795
+ OR ${this.tableName}.updated_at < EXCLUDED.updated_at
42796
+ OR (${this.tableName}.updated_at = EXCLUDED.updated_at
42797
+ AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))
42798
+ RETURNING object_type, object_id`, [this.service, jsonbParam(records), this.machineId(context)]);
42799
+ if (result.rows.length !== records.length) {
42800
+ throw new Error("Template checklist write was rejected before completion; no partial template was committed");
42801
+ }
42802
+ }
42803
+ async deleteTemplateWithTasks(id, context = {}) {
42804
+ await this.ensureSchema();
42805
+ const timestamp3 = new Date().toISOString();
42806
+ const result = await this.options.client.query(`/* todos:delete-template-with-tasks-atomic */ WITH target AS (
42807
+ SELECT 1 FROM ${this.tableName}
42808
+ WHERE service = $1 AND object_type = 'templates' AND object_id = $2 AND deleted_at IS NULL
42809
+ ) UPDATE ${this.tableName} AS record SET
42810
+ deleted_at = $3::timestamptz,
42811
+ updated_at = $3::timestamptz,
42812
+ source_machine_id = COALESCE($4, record.source_machine_id),
42813
+ version = COALESCE(record.version, 0) + 1
42814
+ WHERE record.service = $1 AND record.deleted_at IS NULL AND EXISTS (SELECT 1 FROM target)
42815
+ AND (record.object_type = 'templates' AND record.object_id = $2
42816
+ OR record.object_type = 'template_tasks' AND record.payload->>'template_id' = $2)
42817
+ RETURNING record.object_type`, [this.service, id, timestamp3, this.machineId(context)]);
42818
+ return result.rows.some((row) => row.object_type === "templates");
42819
+ }
42593
42820
  async completeTask(id, agentId, options) {
42594
42821
  await this.ensureSchema();
42595
42822
  const operationTimestamp = new Date().toISOString();
@@ -42930,7 +43157,7 @@ async function updateTask2(id, input, store) {
42930
43157
  tags: input.tags ?? existing.tags,
42931
43158
  metadata: input.metadata ?? existing.metadata,
42932
43159
  requires_approval: input.requires_approval ?? existing.requires_approval,
42933
- task_list_id: input.task_list_id ?? existing.task_list_id
43160
+ task_list_id: input.task_list_id !== undefined ? input.task_list_id : existing.task_list_id
42934
43161
  };
42935
43162
  await store.upsert("tasks", task2);
42936
43163
  return task2;
@@ -43352,7 +43579,7 @@ async function updateTaskList2(id, input, store) {
43352
43579
  }
43353
43580
  async function createTemplate2(input, store, context) {
43354
43581
  const timestamp3 = new Date().toISOString();
43355
- return store.upsert("templates", {
43582
+ const template = {
43356
43583
  id: randomUUID3(),
43357
43584
  name: input.name,
43358
43585
  title_pattern: input.title_pattern,
@@ -43367,7 +43594,30 @@ async function createTemplate2(input, store, context) {
43367
43594
  created_at: timestamp3,
43368
43595
  machine_id: store.machineId(context),
43369
43596
  synced_at: null
43370
- }, context);
43597
+ };
43598
+ const tasks = buildTemplateTasks(template.id, input.tasks ?? [], timestamp3);
43599
+ await store.createTemplateWithTasks(template, tasks, context);
43600
+ return template;
43601
+ }
43602
+ function buildTemplateTasks(templateId, inputs, timestamp3) {
43603
+ return inputs.map((input, position) => ({
43604
+ id: randomUUID3(),
43605
+ template_id: templateId,
43606
+ position,
43607
+ title_pattern: input.title_pattern,
43608
+ description: input.description ?? null,
43609
+ priority: input.priority ?? "medium",
43610
+ tags: input.tags ?? [],
43611
+ task_type: input.task_type ?? null,
43612
+ condition: input.condition ?? null,
43613
+ include_template_id: input.include_template_id ?? null,
43614
+ depends_on_positions: input.depends_on ?? [],
43615
+ metadata: input.metadata ?? {},
43616
+ created_at: timestamp3
43617
+ }));
43618
+ }
43619
+ async function deleteTemplate2(id, store, context) {
43620
+ return store.deleteTemplateWithTasks(id, context);
43371
43621
  }
43372
43622
  async function updateTemplate2(id, input, store) {
43373
43623
  const template = await store.get("templates", id);
@@ -43423,6 +43673,7 @@ async function exportSnapshot(store) {
43423
43673
  agents: await store.list("agents"),
43424
43674
  taskLists: await store.list("task_lists"),
43425
43675
  templates: await store.list("templates"),
43676
+ templateTasks: await store.list("template_tasks"),
43426
43677
  auditHistory: await store.list("audit_history"),
43427
43678
  tombstones: await store.listTombstones()
43428
43679
  };
@@ -43447,6 +43698,7 @@ async function importSnapshot(snapshot, store, context) {
43447
43698
  ...snapshot.agents.map((row) => ["agents", row]),
43448
43699
  ...snapshot.taskLists.map((row) => ["task_lists", row]),
43449
43700
  ...snapshot.templates.map((row) => ["templates", row]),
43701
+ ...(snapshot.templateTasks ?? []).map((row) => ["template_tasks", row]),
43450
43702
  ...snapshot.auditHistory.map((row) => ["audit_history", row])
43451
43703
  ];
43452
43704
  for (const [type, row] of entries) {
@@ -43853,12 +44105,16 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
43853
44105
  TaskList: taskListSchema,
43854
44106
  TaskComment: taskCommentSchema,
43855
44107
  Plan: planSchema,
44108
+ Template: templateSchema,
44109
+ TemplateTask: templateTaskSchema,
44110
+ TemplateVariable: templateVariableSchema,
44111
+ CreateTemplateTaskInput: createTemplateTaskInputSchema,
43856
44112
  CreateTaskInput: {
43857
44113
  type: "object",
43858
44114
  required: ["title"],
43859
44115
  properties: {
43860
44116
  title: { type: "string" },
43861
- description: { type: "string" },
44117
+ description: { type: "string", nullable: true },
43862
44118
  status: { type: "string" },
43863
44119
  priority: { type: "string" },
43864
44120
  project_id: { type: "string" },
@@ -43875,6 +44131,8 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
43875
44131
  status: { type: "string" },
43876
44132
  priority: { type: "string" },
43877
44133
  assigned_to: { type: "string" },
44134
+ project_id: { type: "string", nullable: true },
44135
+ task_list_id: { type: "string", nullable: true },
43878
44136
  version: { type: "number" }
43879
44137
  }
43880
44138
  },
@@ -43991,6 +44249,39 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
43991
44249
  agent_id: { type: "string", minLength: 1 },
43992
44250
  status: { type: "string", enum: ["active", "completed", "archived"] }
43993
44251
  }
44252
+ },
44253
+ CreateTemplateInput: {
44254
+ type: "object",
44255
+ additionalProperties: false,
44256
+ required: ["name", "title_pattern"],
44257
+ properties: {
44258
+ name: { type: "string", minLength: 1 },
44259
+ title_pattern: { type: "string", minLength: 1 },
44260
+ description: { type: "string", nullable: true },
44261
+ priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
44262
+ tags: { type: "array", items: { type: "string", minLength: 1 } },
44263
+ variables: { type: "array", items: { $ref: "#/components/schemas/TemplateVariable" } },
44264
+ project_id: { type: "string", minLength: 1, nullable: true },
44265
+ plan_id: { type: "string", minLength: 1, nullable: true },
44266
+ metadata: { type: "object", additionalProperties: true },
44267
+ tasks: { type: "array", items: { $ref: "#/components/schemas/CreateTemplateTaskInput" } }
44268
+ }
44269
+ },
44270
+ UpdateTemplateInput: {
44271
+ type: "object",
44272
+ additionalProperties: false,
44273
+ minProperties: 1,
44274
+ properties: {
44275
+ name: { type: "string", minLength: 1 },
44276
+ title_pattern: { type: "string", minLength: 1 },
44277
+ description: { type: "string", nullable: true },
44278
+ priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
44279
+ tags: { type: "array", items: { type: "string", minLength: 1 } },
44280
+ variables: { type: "array", items: { type: "object" } },
44281
+ project_id: { type: "string", nullable: true },
44282
+ plan_id: { type: "string", nullable: true },
44283
+ metadata: { type: "object", additionalProperties: true }
44284
+ }
43994
44285
  }
43995
44286
  }
43996
44287
  },
@@ -44293,6 +44584,41 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
44293
44584
  responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { deleted: { type: "boolean" }, id: { type: "string" } } } } } } }
44294
44585
  }
44295
44586
  },
44587
+ "/v1/templates": {
44588
+ get: {
44589
+ operationId: "listTemplates",
44590
+ summary: "List reusable task templates",
44591
+ parameters: [{ name: "project_id", in: "query", schema: { type: "string" } }],
44592
+ responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { templates: { type: "array", items: { $ref: "#/components/schemas/Template" } }, count: { type: "number" } } } } } } }
44593
+ },
44594
+ post: {
44595
+ operationId: "createTemplate",
44596
+ summary: "Create a reusable task template",
44597
+ requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/CreateTemplateInput" } } } },
44598
+ responses: { "201": { content: { "application/json": { schema: { type: "object", properties: { template: { $ref: "#/components/schemas/Template" } } } } } } }
44599
+ }
44600
+ },
44601
+ "/v1/templates/{id}": {
44602
+ get: {
44603
+ operationId: "getTemplate",
44604
+ summary: "Get one reusable task template with its checklist steps",
44605
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
44606
+ responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { template: { $ref: "#/components/schemas/Template" } } } } } } }
44607
+ },
44608
+ patch: {
44609
+ operationId: "updateTemplate",
44610
+ summary: "Update reusable template metadata and defaults",
44611
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
44612
+ requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/UpdateTemplateInput" } } } },
44613
+ responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { template: { $ref: "#/components/schemas/Template" } } } } } } }
44614
+ },
44615
+ delete: {
44616
+ operationId: "deleteTemplate",
44617
+ summary: "Delete a reusable task template and its checklist steps",
44618
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
44619
+ responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { deleted: { type: "boolean" }, id: { type: "string" } } } } } } }
44620
+ }
44621
+ },
44296
44622
  "/v1/task-lists": {
44297
44623
  get: {
44298
44624
  operationId: "listTaskLists",
@@ -44370,6 +44696,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
44370
44696
  agents: { type: "array", items: { type: "object" } },
44371
44697
  taskLists: { type: "array", items: { type: "object" } },
44372
44698
  templates: { type: "array", items: { type: "object" } },
44699
+ templateTasks: { type: "array", items: { $ref: "#/components/schemas/TemplateTask" } },
44373
44700
  auditHistory: { type: "array", items: { type: "object" } },
44374
44701
  tombstones: { type: "array", items: { type: "object" } }
44375
44702
  }
@@ -44406,7 +44733,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
44406
44733
  }
44407
44734
  };
44408
44735
  }
44409
- var taskSchema, projectSchema, taskListSchema, taskCommentSchema, planSchema;
44736
+ var taskSchema, projectSchema, taskListSchema, taskCommentSchema, planSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
44410
44737
  var init_openapi = __esm(() => {
44411
44738
  init_package_version();
44412
44739
  taskSchema = {
@@ -44483,6 +44810,72 @@ var init_openapi = __esm(() => {
44483
44810
  updated_at: { type: "string", format: "date-time" }
44484
44811
  }
44485
44812
  };
44813
+ templateTaskSchema = {
44814
+ type: "object",
44815
+ required: ["id", "template_id", "position", "title_pattern", "priority", "tags", "depends_on_positions", "metadata", "created_at"],
44816
+ properties: {
44817
+ id: { type: "string" },
44818
+ template_id: { type: "string" },
44819
+ position: { type: "integer", minimum: 0 },
44820
+ title_pattern: { type: "string" },
44821
+ description: { type: "string", nullable: true },
44822
+ priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
44823
+ tags: { type: "array", items: { type: "string" } },
44824
+ task_type: { type: "string", nullable: true },
44825
+ condition: { type: "string", nullable: true },
44826
+ include_template_id: { type: "string", nullable: true },
44827
+ depends_on_positions: { type: "array", items: { type: "integer", minimum: 0 } },
44828
+ metadata: { type: "object", additionalProperties: true },
44829
+ created_at: { type: "string", format: "date-time" }
44830
+ }
44831
+ };
44832
+ templateSchema = {
44833
+ type: "object",
44834
+ required: ["id", "name", "title_pattern", "priority", "tags", "variables", "version", "metadata", "created_at"],
44835
+ properties: {
44836
+ id: { type: "string" },
44837
+ name: { type: "string" },
44838
+ title_pattern: { type: "string" },
44839
+ description: { type: "string", nullable: true },
44840
+ priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
44841
+ tags: { type: "array", items: { type: "string" } },
44842
+ variables: { type: "array", items: { type: "object", properties: { name: { type: "string" }, required: { type: "boolean" }, default: { type: "string" }, description: { type: "string" } } } },
44843
+ version: { type: "integer", minimum: 1 },
44844
+ project_id: { type: "string", nullable: true },
44845
+ plan_id: { type: "string", nullable: true },
44846
+ metadata: { type: "object", additionalProperties: true },
44847
+ created_at: { type: "string", format: "date-time" },
44848
+ tasks: { type: "array", items: { $ref: "#/components/schemas/TemplateTask" } }
44849
+ }
44850
+ };
44851
+ templateVariableSchema = {
44852
+ type: "object",
44853
+ required: ["name", "required"],
44854
+ properties: {
44855
+ name: { type: "string" },
44856
+ required: { type: "boolean" },
44857
+ default: { type: "string" },
44858
+ description: { type: "string" }
44859
+ }
44860
+ };
44861
+ createTemplateTaskInputSchema = {
44862
+ type: "object",
44863
+ additionalProperties: false,
44864
+ required: ["title_pattern"],
44865
+ properties: {
44866
+ position: { type: "integer", minimum: 0 },
44867
+ title_pattern: { type: "string", minLength: 1 },
44868
+ description: { type: "string", nullable: true },
44869
+ priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
44870
+ tags: { type: "array", items: { type: "string", minLength: 1 } },
44871
+ task_type: { type: "string", nullable: true },
44872
+ condition: { type: "string", nullable: true },
44873
+ include_template_id: { type: "string", nullable: true },
44874
+ depends_on: { type: "array", items: { type: "integer", minimum: 0 } },
44875
+ depends_on_positions: { type: "array", items: { type: "integer", minimum: 0 } },
44876
+ metadata: { type: "object", additionalProperties: true }
44877
+ }
44878
+ };
44486
44879
  });
44487
44880
 
44488
44881
  // src/server/v1.ts
@@ -44622,6 +45015,122 @@ function validatePlanCreate(value) {
44622
45015
  }
44623
45016
  };
44624
45017
  }
45018
+ function validateTemplateTask(value) {
45019
+ if (!value || typeof value !== "object" || Array.isArray(value))
45020
+ return null;
45021
+ const body = value;
45022
+ const allowed = new Set(["position", "title_pattern", "description", "priority", "tags", "task_type", "condition", "include_template_id", "depends_on", "depends_on_positions", "metadata"]);
45023
+ if (Object.keys(body).some((key) => !allowed.has(key)))
45024
+ return null;
45025
+ if (typeof body.title_pattern !== "string" || !body.title_pattern.trim())
45026
+ return null;
45027
+ if (body.position !== undefined && (typeof body.position !== "number" || !Number.isSafeInteger(body.position) || body.position < 0))
45028
+ return null;
45029
+ if (body.description !== undefined && body.description !== null && typeof body.description !== "string")
45030
+ return null;
45031
+ if (body.priority !== undefined && (typeof body.priority !== "string" || !["low", "medium", "high", "critical"].includes(body.priority)))
45032
+ return null;
45033
+ if (body.tags !== undefined && (!Array.isArray(body.tags) || body.tags.some((tag) => typeof tag !== "string" || !tag.trim())))
45034
+ return null;
45035
+ for (const field of ["task_type", "condition", "include_template_id"]) {
45036
+ if (body[field] !== undefined && body[field] !== null && (typeof body[field] !== "string" || !body[field].trim()))
45037
+ return null;
45038
+ }
45039
+ if (body.depends_on !== undefined && body.depends_on_positions !== undefined)
45040
+ return null;
45041
+ const dependencies = body.depends_on ?? body.depends_on_positions;
45042
+ if (dependencies !== undefined && (!Array.isArray(dependencies) || dependencies.some((position) => !Number.isSafeInteger(position) || position < 0)))
45043
+ return null;
45044
+ if (body.metadata !== undefined && (!body.metadata || typeof body.metadata !== "object" || Array.isArray(body.metadata)))
45045
+ return null;
45046
+ return {
45047
+ title_pattern: body.title_pattern,
45048
+ ...typeof body.description === "string" ? { description: body.description } : {},
45049
+ ...typeof body.priority === "string" ? { priority: body.priority } : {},
45050
+ ...Array.isArray(body.tags) ? { tags: body.tags } : {},
45051
+ ...typeof body.task_type === "string" ? { task_type: body.task_type } : {},
45052
+ ...typeof body.condition === "string" ? { condition: body.condition } : {},
45053
+ ...typeof body.include_template_id === "string" ? { include_template_id: body.include_template_id } : {},
45054
+ ...Array.isArray(dependencies) ? { depends_on: dependencies } : {},
45055
+ ...body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata) ? { metadata: body.metadata } : {}
45056
+ };
45057
+ }
45058
+ function validateTemplateCreate(value) {
45059
+ if (!value || typeof value !== "object" || Array.isArray(value))
45060
+ return { ok: false, message: "template body must be an object" };
45061
+ const body = value;
45062
+ const allowed = new Set(["name", "title_pattern", "description", "priority", "tags", "variables", "project_id", "plan_id", "metadata", "tasks"]);
45063
+ const unknown = Object.keys(body).find((key) => !allowed.has(key));
45064
+ if (unknown)
45065
+ return { ok: false, message: `unknown template field: ${unknown}` };
45066
+ if (typeof body.name !== "string" || !body.name.trim())
45067
+ return { ok: false, message: "name must be a non-empty string" };
45068
+ if (typeof body.title_pattern !== "string" || !body.title_pattern.trim())
45069
+ return { ok: false, message: "title_pattern must be a non-empty string" };
45070
+ if (body.description !== undefined && body.description !== null && typeof body.description !== "string")
45071
+ return { ok: false, message: "description must be a string or null" };
45072
+ if (body.priority !== undefined && (typeof body.priority !== "string" || !["low", "medium", "high", "critical"].includes(body.priority)))
45073
+ return { ok: false, message: "priority must be low, medium, high, or critical" };
45074
+ if (body.tags !== undefined && (!Array.isArray(body.tags) || body.tags.some((tag) => typeof tag !== "string" || !tag.trim())))
45075
+ return { ok: false, message: "tags must be an array of non-empty strings" };
45076
+ if (body.variables !== undefined && (!Array.isArray(body.variables) || body.variables.some((variable) => !variable || typeof variable !== "object" || Array.isArray(variable) || typeof variable.name !== "string" || !variable.name || typeof variable.required !== "boolean" || variable.default !== undefined && typeof variable.default !== "string" || variable.description !== undefined && typeof variable.description !== "string"))) {
45077
+ return { ok: false, message: "variables must be valid template variable objects" };
45078
+ }
45079
+ for (const field of ["project_id", "plan_id"]) {
45080
+ if (body[field] !== undefined && body[field] !== null && (typeof body[field] !== "string" || !body[field].trim()))
45081
+ return { ok: false, message: `${field} must be a non-empty string or null` };
45082
+ }
45083
+ if (body.metadata !== undefined && (!body.metadata || typeof body.metadata !== "object" || Array.isArray(body.metadata)))
45084
+ return { ok: false, message: "metadata must be an object" };
45085
+ const tasks = body.tasks === undefined ? [] : Array.isArray(body.tasks) ? body.tasks.map(validateTemplateTask) : null;
45086
+ if (tasks === null || tasks.some((task2) => task2 === null))
45087
+ return { ok: false, message: "tasks must be valid template task objects" };
45088
+ const taskInputs = tasks;
45089
+ for (const [position, task2] of taskInputs.entries()) {
45090
+ if ((task2.depends_on ?? []).some((dependency) => dependency >= position)) {
45091
+ return { ok: false, message: "template task dependencies must reference earlier task positions" };
45092
+ }
45093
+ }
45094
+ return {
45095
+ ok: true,
45096
+ input: {
45097
+ name: body.name,
45098
+ title_pattern: body.title_pattern,
45099
+ ...typeof body.description === "string" ? { description: body.description } : {},
45100
+ ...typeof body.priority === "string" ? { priority: body.priority } : {},
45101
+ ...Array.isArray(body.tags) ? { tags: body.tags } : {},
45102
+ ...Array.isArray(body.variables) ? { variables: body.variables } : {},
45103
+ ...typeof body.project_id === "string" ? { project_id: body.project_id } : {},
45104
+ ...typeof body.plan_id === "string" ? { plan_id: body.plan_id } : {},
45105
+ ...body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata) ? { metadata: body.metadata } : {},
45106
+ tasks: taskInputs
45107
+ }
45108
+ };
45109
+ }
45110
+ function validateTemplatePatch(value) {
45111
+ if (!value || typeof value !== "object" || Array.isArray(value))
45112
+ return { ok: false, message: "template patch must be an object" };
45113
+ const body = value;
45114
+ const allowed = new Set(["name", "title_pattern", "description", "priority", "tags", "variables", "project_id", "plan_id", "metadata"]);
45115
+ const unknown = Object.keys(body).find((key) => !allowed.has(key));
45116
+ if (unknown)
45117
+ return { ok: false, message: `unknown template field: ${unknown}` };
45118
+ if (Object.keys(body).length === 0)
45119
+ return { ok: false, message: "template patch must not be empty" };
45120
+ const templateLike = { name: body.name ?? "template", title_pattern: body.title_pattern ?? "template", ...body };
45121
+ const validated = validateTemplateCreate(templateLike);
45122
+ if (!validated.ok)
45123
+ return validated;
45124
+ const { name: _name, title_pattern: _title, tasks: _tasks, ...patch } = validated.input;
45125
+ return { ok: true, patch: {
45126
+ ...body.name !== undefined ? { name: validated.input.name } : {},
45127
+ ...body.title_pattern !== undefined ? { title_pattern: validated.input.title_pattern } : {},
45128
+ ...patch,
45129
+ ...body.description === null ? { description: null } : {},
45130
+ ...body.project_id === null ? { project_id: null } : {},
45131
+ ...body.plan_id === null ? { plan_id: null } : {}
45132
+ } };
45133
+ }
44625
45134
  async function readJson(req) {
44626
45135
  try {
44627
45136
  const text2 = await req.text();
@@ -44682,12 +45191,13 @@ function normalizeImportSnapshot(raw) {
44682
45191
  agents: arr(body["agents"]),
44683
45192
  taskLists: arr(body["taskLists"]),
44684
45193
  templates: arr(body["templates"]),
45194
+ templateTasks: arr(body["templateTasks"]),
44685
45195
  auditHistory: arr(body["auditHistory"]),
44686
45196
  tombstones: arr(body["tombstones"])
44687
45197
  };
44688
45198
  }
44689
45199
  function countSnapshotRecords(s) {
44690
- return s.tasks.length + s.projects.length + (s.projectMachinePaths?.length ?? 0) + s.plans.length + s.agents.length + s.taskLists.length + s.templates.length + s.auditHistory.length + (s.tombstones?.length ?? 0);
45200
+ return s.tasks.length + s.projects.length + (s.projectMachinePaths?.length ?? 0) + s.plans.length + s.agents.length + s.taskLists.length + s.templates.length + s.templateTasks.length + s.auditHistory.length + (s.tombstones?.length ?? 0);
44691
45201
  }
44692
45202
  async function handleV1Request(req, url, dependencies = {}) {
44693
45203
  const path = url.pathname;
@@ -45257,6 +45767,40 @@ async function handleV1Request(req, url, dependencies = {}) {
45257
45767
  if (id)
45258
45768
  return error(405, `method ${method} not allowed on /v1/plans/:id`);
45259
45769
  }
45770
+ if (resource === "templates") {
45771
+ if (!id && method === "GET") {
45772
+ const projectId = url.searchParams.get("project_id");
45773
+ const templates = (await store.templates.list()).filter((template) => projectId === null || template.project_id === projectId);
45774
+ return json2({ templates, count: templates.length });
45775
+ }
45776
+ if (!id && method === "POST") {
45777
+ const body = await readJson(req);
45778
+ const validated = validateTemplateCreate(body);
45779
+ if (!validated.ok)
45780
+ return error(400, validated.message);
45781
+ const template = await store.templates.create(validated.input, contextFromPrincipal(principal));
45782
+ return json2({ template: await store.templates.getWithTasks(template.id) }, 201);
45783
+ }
45784
+ if (!id)
45785
+ return error(405, `method ${method} not allowed on /v1/templates`);
45786
+ if (method === "GET") {
45787
+ const template = await store.templates.getWithTasks(id);
45788
+ return template ? json2({ template }) : error(404, "template not found");
45789
+ }
45790
+ if (method === "PATCH" || method === "PUT") {
45791
+ const body = await readJson(req);
45792
+ const validated = validateTemplatePatch(body);
45793
+ if (!validated.ok)
45794
+ return error(400, validated.message);
45795
+ const template = await store.templates.update(id, validated.patch, contextFromPrincipal(principal));
45796
+ return template ? json2({ template: await store.templates.getWithTasks(id) }) : error(404, "template not found");
45797
+ }
45798
+ if (method === "DELETE") {
45799
+ const deleted = await store.templates.delete(id, contextFromPrincipal(principal));
45800
+ return deleted ? json2({ deleted: true, id }) : error(404, "template not found");
45801
+ }
45802
+ return error(405, `method ${method} not allowed on /v1/templates/:id`);
45803
+ }
45260
45804
  if (resource === "agents") {
45261
45805
  if (!id && method === "GET") {
45262
45806
  const agents = await store.agents.list();