@hasna/todos 0.11.94 → 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.
package/dist/cli/index.js CHANGED
@@ -2104,7 +2104,7 @@ var package_default;
2104
2104
  var init_package = __esm(() => {
2105
2105
  package_default = {
2106
2106
  name: "@hasna/todos",
2107
- version: "0.11.94",
2107
+ version: "0.11.95",
2108
2108
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
2109
2109
  type: "module",
2110
2110
  main: "dist/index.js",
@@ -2826,6 +2826,48 @@ async function cloudDeleteTask(client, id) {
2826
2826
  throw error;
2827
2827
  }
2828
2828
  }
2829
+ function unwrapTemplate(raw) {
2830
+ if (raw && typeof raw === "object" && "template" in raw) {
2831
+ return raw.template;
2832
+ }
2833
+ return raw;
2834
+ }
2835
+ async function cloudListTemplates(client, projectId) {
2836
+ const result = await requiredRemoteRoute(client, "/v1/templates", () => client.list("templates", { query: projectId ? { project_id: projectId } : undefined }));
2837
+ const envelope = result.raw;
2838
+ return Array.isArray(envelope?.templates) ? envelope.templates : result.items;
2839
+ }
2840
+ async function cloudCreateTemplate(client, input) {
2841
+ return unwrapTemplate(await requiredRemoteRoute(client, "/v1/templates", () => client.create("templates", input)));
2842
+ }
2843
+ async function cloudGetTemplate(client, id) {
2844
+ try {
2845
+ return unwrapTemplate(await client.get("templates", id));
2846
+ } catch (error) {
2847
+ if (error && typeof error === "object" && error.status === 404)
2848
+ return null;
2849
+ throw error;
2850
+ }
2851
+ }
2852
+ async function cloudUpdateTemplate(client, id, patch) {
2853
+ try {
2854
+ return unwrapTemplate(await client.update("templates", id, patch));
2855
+ } catch (error) {
2856
+ if (error && typeof error === "object" && error.status === 404)
2857
+ return null;
2858
+ throw error;
2859
+ }
2860
+ }
2861
+ async function cloudDeleteTemplate(client, id) {
2862
+ try {
2863
+ await client.transport.del(`/templates/${encodeURIComponent(id)}`);
2864
+ return true;
2865
+ } catch (error) {
2866
+ if (error && typeof error === "object" && error.status === 404)
2867
+ return false;
2868
+ throw error;
2869
+ }
2870
+ }
2829
2871
  async function cloudTaskAction(client, id, action, body = {}) {
2830
2872
  const raw = await client.transport.post(`/tasks/${encodeURIComponent(id)}/${action}`, body);
2831
2873
  return unwrapTask(raw);
@@ -9928,6 +9970,50 @@ var init_recurrence = __esm(() => {
9928
9970
  };
9929
9971
  });
9930
9972
 
9973
+ // src/lib/template-semantics.ts
9974
+ function resolveTemplateVariables(templateVars, provided) {
9975
+ const merged = { ...provided };
9976
+ for (const variable of templateVars) {
9977
+ if (merged[variable.name] === undefined && variable.default !== undefined) {
9978
+ merged[variable.name] = variable.default;
9979
+ }
9980
+ }
9981
+ const missing = templateVars.filter((variable) => variable.required && merged[variable.name] === undefined).map((variable) => variable.name);
9982
+ if (missing.length > 0) {
9983
+ throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
9984
+ }
9985
+ return merged;
9986
+ }
9987
+ function substituteTemplateVariables(text, variables) {
9988
+ let result = text;
9989
+ for (const [key, value] of Object.entries(variables)) {
9990
+ result = result.replace(new RegExp(`\\{${key}\\}`, "g"), value);
9991
+ }
9992
+ return result;
9993
+ }
9994
+ function evaluateTemplateCondition(condition, variables) {
9995
+ if (!condition || condition.trim() === "")
9996
+ return true;
9997
+ const trimmed = condition.trim();
9998
+ const equal = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
9999
+ if (equal)
10000
+ return (variables[equal[1]] ?? "") === equal[2].trim();
10001
+ const unequal = trimmed.match(/^\{([^}]+)\}\s*!=\s*(.+)$/);
10002
+ if (unequal)
10003
+ return (variables[unequal[1]] ?? "") !== unequal[2].trim();
10004
+ const falsy = trimmed.match(/^!\{([^}]+)\}$/);
10005
+ if (falsy) {
10006
+ const value = variables[falsy[1]];
10007
+ return !value || value === "" || value === "false";
10008
+ }
10009
+ const truthy = trimmed.match(/^\{([^}]+)\}$/);
10010
+ if (truthy) {
10011
+ const value = variables[truthy[1]];
10012
+ return !!value && value !== "" && value !== "false";
10013
+ }
10014
+ return true;
10015
+ }
10016
+
9931
10017
  // src/db/templates.ts
9932
10018
  var exports_templates = {};
9933
10019
  __export(exports_templates, {
@@ -10023,6 +10109,14 @@ function deleteTemplate(id, db) {
10023
10109
  payload: template,
10024
10110
  version: template.version
10025
10111
  }, d);
10112
+ for (const task of getTemplateTasks(resolved, d)) {
10113
+ recordStorageTombstone({
10114
+ object_type: "template_tasks",
10115
+ object_id: task.id,
10116
+ payload: task,
10117
+ version: 1
10118
+ }, d);
10119
+ }
10026
10120
  return d.run("DELETE FROM task_templates WHERE id = ?", [resolved]).changes > 0;
10027
10121
  }
10028
10122
  function updateTemplate(id, updates, db) {
@@ -10154,34 +10248,7 @@ function getTemplateTasks(templateId, db) {
10154
10248
  return rows.map(rowToTemplateTask);
10155
10249
  }
10156
10250
  function evaluateCondition(condition, variables) {
10157
- if (!condition || condition.trim() === "")
10158
- return true;
10159
- const trimmed = condition.trim();
10160
- const eqMatch = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
10161
- if (eqMatch) {
10162
- const varName = eqMatch[1];
10163
- const expected = eqMatch[2].trim();
10164
- return (variables[varName] ?? "") === expected;
10165
- }
10166
- const neqMatch = trimmed.match(/^\{([^}]+)\}\s*!=\s*(.+)$/);
10167
- if (neqMatch) {
10168
- const varName = neqMatch[1];
10169
- const expected = neqMatch[2].trim();
10170
- return (variables[varName] ?? "") !== expected;
10171
- }
10172
- const falsyMatch = trimmed.match(/^!\{([^}]+)\}$/);
10173
- if (falsyMatch) {
10174
- const varName = falsyMatch[1];
10175
- const val = variables[varName];
10176
- return !val || val === "" || val === "false";
10177
- }
10178
- const truthyMatch = trimmed.match(/^\{([^}]+)\}$/);
10179
- if (truthyMatch) {
10180
- const varName = truthyMatch[1];
10181
- const val = variables[varName];
10182
- return !!val && val !== "" && val !== "false";
10183
- }
10184
- return true;
10251
+ return evaluateTemplateCondition(condition, variables);
10185
10252
  }
10186
10253
  function exportTemplate(id, db) {
10187
10254
  const d = db || getDatabase();
@@ -10254,29 +10321,10 @@ function listTemplateVersions(id, db) {
10254
10321
  return d.query("SELECT * FROM template_versions WHERE template_id = ? ORDER BY version DESC").all(resolved);
10255
10322
  }
10256
10323
  function resolveVariables(templateVars, provided) {
10257
- const merged = { ...provided };
10258
- for (const v of templateVars) {
10259
- if (merged[v.name] === undefined && v.default !== undefined) {
10260
- merged[v.name] = v.default;
10261
- }
10262
- }
10263
- const missing = [];
10264
- for (const v of templateVars) {
10265
- if (v.required && merged[v.name] === undefined) {
10266
- missing.push(v.name);
10267
- }
10268
- }
10269
- if (missing.length > 0) {
10270
- throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
10271
- }
10272
- return merged;
10324
+ return resolveTemplateVariables(templateVars, provided);
10273
10325
  }
10274
10326
  function substituteVars(text, variables) {
10275
- let result = text;
10276
- for (const [key, val] of Object.entries(variables)) {
10277
- result = result.replace(new RegExp(`\\{${key}\\}`, "g"), val);
10278
- }
10279
- return result;
10327
+ return substituteTemplateVariables(text, variables);
10280
10328
  }
10281
10329
  function tasksFromTemplate(templateId, projectId, variables, taskListId, db, _visitedTemplateIds) {
10282
10330
  const d = db || getDatabase();
@@ -16910,6 +16958,80 @@ __export(exports_plan_template_commands, {
16910
16958
  registerPlanTemplateCommands: () => registerPlanTemplateCommands
16911
16959
  });
16912
16960
  import chalk3 from "chalk";
16961
+ async function createRemoteTemplateTasks(cloud, template, projectId, variables, agentId, overrides, visited = new Set) {
16962
+ if (visited.has(template.id)) {
16963
+ throw new Error(`Circular template reference detected: ${template.id}`);
16964
+ }
16965
+ visited.add(template.id);
16966
+ try {
16967
+ const resolved = resolveTemplateVariables(template.variables ?? [], variables);
16968
+ const render = (value) => value === null || value === undefined ? value : substituteTemplateVariables(value, resolved);
16969
+ if (template.tasks.length === 0) {
16970
+ const task = await cloudCreateTask(cloud, {
16971
+ title: render(overrides?.title || template.title_pattern),
16972
+ ...render(overrides?.description ?? template.description) ? { description: render(overrides?.description ?? template.description) } : {},
16973
+ priority: overrides?.priority ?? template.priority,
16974
+ tags: template.tags,
16975
+ ...projectId ? { project_id: projectId } : {},
16976
+ ...template.plan_id ? { plan_id: template.plan_id } : {},
16977
+ ...agentId ? { agent_id: agentId } : {},
16978
+ ...Object.keys(template.metadata ?? {}).length > 0 ? { metadata: template.metadata } : {}
16979
+ });
16980
+ return { tasks: [task] };
16981
+ }
16982
+ const created = [];
16983
+ const positionToTaskId = new Map;
16984
+ const skippedPositions = new Set;
16985
+ for (const step of template.tasks) {
16986
+ if (step.include_template_id) {
16987
+ const included = await cloudGetTemplate(cloud, step.include_template_id);
16988
+ if (!included)
16989
+ throw new Error(`Included template not found: ${step.include_template_id}`);
16990
+ const result = await createRemoteTemplateTasks(cloud, included, projectId, resolved, agentId, undefined, visited);
16991
+ created.push(...result.tasks);
16992
+ if (result.tasks.length > 0)
16993
+ positionToTaskId.set(step.position, result.tasks[0].id);
16994
+ else
16995
+ skippedPositions.add(step.position);
16996
+ continue;
16997
+ }
16998
+ if (step.condition && !evaluateTemplateCondition(step.condition, resolved)) {
16999
+ skippedPositions.add(step.position);
17000
+ continue;
17001
+ }
17002
+ const task = await cloudCreateTask(cloud, {
17003
+ title: render(step.title_pattern),
17004
+ ...render(step.description) ? { description: render(step.description) } : {},
17005
+ priority: step.priority,
17006
+ tags: step.tags,
17007
+ ...step.task_type ? { task_type: step.task_type } : {},
17008
+ ...projectId ? { project_id: projectId } : {},
17009
+ ...template.plan_id ? { plan_id: template.plan_id } : {},
17010
+ ...agentId ? { agent_id: agentId } : {},
17011
+ ...Object.keys(step.metadata ?? {}).length > 0 ? { metadata: step.metadata } : {}
17012
+ });
17013
+ created.push(task);
17014
+ positionToTaskId.set(step.position, task.id);
17015
+ }
17016
+ for (const step of template.tasks) {
17017
+ if (skippedPositions.has(step.position) || step.include_template_id)
17018
+ continue;
17019
+ const taskId = positionToTaskId.get(step.position);
17020
+ if (!taskId)
17021
+ continue;
17022
+ for (const dependencyPosition of step.depends_on_positions) {
17023
+ if (skippedPositions.has(dependencyPosition))
17024
+ continue;
17025
+ const dependencyId = positionToTaskId.get(dependencyPosition);
17026
+ if (dependencyId)
17027
+ await cloudAddDependency(cloud, taskId, dependencyId);
17028
+ }
17029
+ }
17030
+ return { tasks: created };
17031
+ } finally {
17032
+ visited.delete(template.id);
17033
+ }
17034
+ }
16913
17035
  function resolvePlanCliRef(ref, projectId) {
16914
17036
  const db = getDatabase();
16915
17037
  const resolved = resolvePlanRefDetailed(ref, db, projectId);
@@ -17154,6 +17276,117 @@ function registerPlanTemplateCommands(program2) {
17154
17276
  });
17155
17277
  program2.command("templates").description("List and manage task templates").option("--add <name>", "Create a template").option("--title <pattern>", "Title pattern (with --add)").option("-d, --description <text>", "Default description").option("-p, --priority <level>", "Default priority").option("-t, --tags <tags>", "Default tags (comma-separated)").option("--delete <id>", "Delete a template").option("--update <id>", "Update a template").option("--use <id>", "Create a task from a template").option("--var <vars...>", "Variable substitutions: key=value (e.g. --var feature=login)").action(async (opts) => {
17156
17278
  const globalOpts = program2.opts();
17279
+ const cloud = getTodosCloudClient();
17280
+ if (cloud) {
17281
+ try {
17282
+ const projectId = globalOpts.project ? await cloudResolveProjectRef(cloud, globalOpts.project) : undefined;
17283
+ if (opts.add) {
17284
+ if (!opts.title) {
17285
+ console.error(chalk3.red("--title is required with --add"));
17286
+ process.exit(1);
17287
+ }
17288
+ const template = await cloudCreateTemplate(cloud, {
17289
+ name: opts.add,
17290
+ title_pattern: opts.title,
17291
+ description: opts.description,
17292
+ priority: opts.priority || "medium",
17293
+ tags: opts.tags ? opts.tags.split(",").map((tag) => tag.trim()).filter(Boolean) : [],
17294
+ project_id: projectId
17295
+ });
17296
+ if (globalOpts.json) {
17297
+ output(template, true);
17298
+ } else {
17299
+ console.log(chalk3.green(`Template created: ${template.id.slice(0, 8)} | ${template.name} | "${template.title_pattern}"`));
17300
+ }
17301
+ return;
17302
+ }
17303
+ if (opts.delete) {
17304
+ const deleted = await cloudDeleteTemplate(cloud, opts.delete);
17305
+ if (globalOpts.json) {
17306
+ output({ deleted }, true);
17307
+ } else if (deleted) {
17308
+ console.log(chalk3.green("Template deleted."));
17309
+ } else {
17310
+ console.error(chalk3.red("Template not found."));
17311
+ process.exit(1);
17312
+ }
17313
+ return;
17314
+ }
17315
+ if (opts.update) {
17316
+ const updates = {};
17317
+ if (opts.title)
17318
+ updates.title_pattern = opts.title;
17319
+ if (opts.description)
17320
+ updates.description = opts.description;
17321
+ if (opts.priority)
17322
+ updates.priority = opts.priority;
17323
+ if (opts.tags)
17324
+ updates.tags = opts.tags.split(",").map((tag) => tag.trim()).filter(Boolean);
17325
+ if (Object.keys(updates).length === 0) {
17326
+ console.error(chalk3.red("Provide --title, --description, --priority, or --tags with --update"));
17327
+ process.exit(1);
17328
+ }
17329
+ const updated = await cloudUpdateTemplate(cloud, opts.update, updates);
17330
+ if (!updated) {
17331
+ console.error(chalk3.red("Template not found."));
17332
+ process.exit(1);
17333
+ }
17334
+ if (globalOpts.json) {
17335
+ output(updated, true);
17336
+ } else {
17337
+ console.log(chalk3.green(`Template updated: ${updated.id.slice(0, 8)} | ${updated.name} | "${updated.title_pattern}"`));
17338
+ }
17339
+ return;
17340
+ }
17341
+ if (opts.use) {
17342
+ const variables = {};
17343
+ for (const value of opts.var ?? []) {
17344
+ const separator = value.indexOf("=");
17345
+ if (separator === -1) {
17346
+ console.error(chalk3.red(`Invalid variable format: ${value} (expected key=value)`));
17347
+ process.exit(1);
17348
+ }
17349
+ variables[value.slice(0, separator)] = value.slice(separator + 1);
17350
+ }
17351
+ const template = await cloudGetTemplate(cloud, opts.use);
17352
+ if (!template) {
17353
+ console.error(chalk3.red("Template not found."));
17354
+ process.exit(1);
17355
+ }
17356
+ const targetProjectId = template.project_id ?? projectId;
17357
+ const { tasks: created } = await createRemoteTemplateTasks(cloud, template, targetProjectId, variables, globalOpts.agent, {
17358
+ title: opts.title,
17359
+ description: opts.description,
17360
+ priority: opts.priority
17361
+ });
17362
+ if (globalOpts.json) {
17363
+ output(created, true);
17364
+ } else {
17365
+ console.log(chalk3.green(`${created.length} task(s) created from template:`));
17366
+ for (const task of created)
17367
+ console.log(formatTaskLine(task));
17368
+ }
17369
+ return;
17370
+ }
17371
+ const templates2 = await cloudListTemplates(cloud, projectId);
17372
+ if (globalOpts.json) {
17373
+ output(templates2, true);
17374
+ return;
17375
+ }
17376
+ if (templates2.length === 0) {
17377
+ console.log(chalk3.dim("No templates."));
17378
+ return;
17379
+ }
17380
+ console.log(chalk3.bold(`${templates2.length} template(s):
17381
+ `));
17382
+ for (const template of templates2) {
17383
+ console.log(` ${chalk3.dim(template.id.slice(0, 8))} ${chalk3.bold(template.name)} ${chalk3.cyan(`"${template.title_pattern}"`)} ${chalk3.yellow(template.priority)}`);
17384
+ }
17385
+ } catch (error) {
17386
+ handleError(error);
17387
+ }
17388
+ return;
17389
+ }
17157
17390
  const {
17158
17391
  createTemplate: createTemplate2,
17159
17392
  getTemplateWithTasks: getTemplateWithTasks2,
@@ -17406,7 +17639,6 @@ function registerPlanTemplateCommands(program2) {
17406
17639
  });
17407
17640
  program2.command("template-import [file]").alias("templates-import").description("Import a template from a JSON file").option("--file <path>", "Path to template JSON file (alternative to positional arg)").action(async (file, opts) => {
17408
17641
  const globalOpts = program2.opts();
17409
- const { importTemplate: importTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
17410
17642
  const { readFileSync: readFileSync5 } = await import("fs");
17411
17643
  try {
17412
17644
  const filePath = file || opts.file;
@@ -17416,7 +17648,8 @@ function registerPlanTemplateCommands(program2) {
17416
17648
  }
17417
17649
  const content = readFileSync5(filePath, "utf-8");
17418
17650
  const json = JSON.parse(content);
17419
- const template = importTemplate2(json);
17651
+ const cloud = getTodosCloudClient();
17652
+ const template = cloud ? await cloudCreateTemplate(cloud, json) : (await Promise.resolve().then(() => (init_templates(), exports_templates))).importTemplate(json);
17420
17653
  if (globalOpts.json) {
17421
17654
  output(template, true);
17422
17655
  } else {
@@ -26311,6 +26544,7 @@ function exportSqliteTodosStorageSnapshot(db) {
26311
26544
  agents: listAgents({ include_archived: true }, d),
26312
26545
  taskLists: listTaskLists(undefined, d),
26313
26546
  templates: listTemplates(d),
26547
+ templateTasks: listTemplates(d).flatMap((template) => getTemplateTasks(template.id, d)),
26314
26548
  auditHistory: getRecentActivity(Number.MAX_SAFE_INTEGER, d),
26315
26549
  tombstones: listStorageTombstones(d)
26316
26550
  };
@@ -26360,6 +26594,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
26360
26594
  applyRows("task_lists", "task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
26361
26595
  applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at");
26362
26596
  applyRows("templates", "task_templates", TEMPLATE_COLUMNS, snapshot.templates);
26597
+ applyRows("template_tasks", "template_tasks", TEMPLATE_TASK_COLUMNS, snapshot.templateTasks ?? []);
26363
26598
  applyRows("tasks", "tasks", TASK_COLUMNS, sortedTasks2(snapshot.tasks), "updated_at", (row, changed) => {
26364
26599
  if (changed && Array.isArray(row["tags"]) && typeof row["id"] === "string") {
26365
26600
  replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
@@ -26464,6 +26699,8 @@ function tableForTombstone(objectType) {
26464
26699
  return "task_lists";
26465
26700
  if (objectType === "templates")
26466
26701
  return "task_templates";
26702
+ if (objectType === "template_tasks")
26703
+ return "template_tasks";
26467
26704
  return "task_history";
26468
26705
  }
26469
26706
  function listRows(db, table, columns) {
@@ -26500,7 +26737,7 @@ function clockColumnsForTable(table) {
26500
26737
  return ["created_at"];
26501
26738
  return ["updated_at", "created_at"];
26502
26739
  }
26503
- var PROJECT_COLUMNS, PROJECT_MACHINE_PATH_COLUMNS, TASK_LIST_COLUMNS, PLAN_COLUMNS, AGENT_COLUMNS, TEMPLATE_COLUMNS, TASK_COLUMNS, AUDIT_COLUMNS, JSON_COLUMNS, BOOLEAN_COLUMNS;
26740
+ 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;
26504
26741
  var init_sqlite_snapshot = __esm(() => {
26505
26742
  init_database();
26506
26743
  init_agents();
@@ -26594,6 +26831,21 @@ var init_sqlite_snapshot = __esm(() => {
26594
26831
  "machine_id",
26595
26832
  "synced_at"
26596
26833
  ];
26834
+ TEMPLATE_TASK_COLUMNS = [
26835
+ "id",
26836
+ "template_id",
26837
+ "position",
26838
+ "title_pattern",
26839
+ "description",
26840
+ "priority",
26841
+ "tags",
26842
+ "task_type",
26843
+ "condition",
26844
+ "include_template_id",
26845
+ "depends_on_positions",
26846
+ "metadata",
26847
+ "created_at"
26848
+ ];
26597
26849
  TASK_COLUMNS = [
26598
26850
  "id",
26599
26851
  "short_id",
@@ -26662,7 +26914,7 @@ var init_sqlite_snapshot = __esm(() => {
26662
26914
  "created_at",
26663
26915
  "machine_id"
26664
26916
  ];
26665
- JSON_COLUMNS = new Set(["tags", "metadata", "permissions", "capabilities", "variables"]);
26917
+ JSON_COLUMNS = new Set(["tags", "metadata", "permissions", "capabilities", "variables", "depends_on_positions"]);
26666
26918
  BOOLEAN_COLUMNS = new Set(["requires_approval"]);
26667
26919
  });
26668
26920
 
@@ -27052,6 +27304,7 @@ function snapshotEntries(snapshot) {
27052
27304
  ...snapshot.agents.map((payload) => entry("agents", payload, snapshot.exportedAt)),
27053
27305
  ...snapshot.taskLists.map((payload) => entry("task_lists", payload, snapshot.exportedAt)),
27054
27306
  ...snapshot.templates.map((payload) => entry("templates", payload, snapshot.exportedAt)),
27307
+ ...(snapshot.templateTasks ?? []).map((payload) => entry("template_tasks", payload, snapshot.exportedAt)),
27055
27308
  ...snapshot.auditHistory.map((payload) => entry("audit_history", payload, snapshot.exportedAt)),
27056
27309
  ...(snapshot.tombstones ?? []).map((tombstone) => ({
27057
27310
  type: tombstone.object_type,
@@ -27103,6 +27356,7 @@ function rowsToSnapshot(rows) {
27103
27356
  agents: [],
27104
27357
  taskLists: [],
27105
27358
  templates: [],
27359
+ templateTasks: [],
27106
27360
  auditHistory: [],
27107
27361
  tombstones: []
27108
27362
  };
@@ -27137,6 +27391,8 @@ function rowsToSnapshot(rows) {
27137
27391
  snapshot.taskLists.push(payload);
27138
27392
  else if (row.object_type === "templates")
27139
27393
  snapshot.templates.push(payload);
27394
+ else if (row.object_type === "template_tasks")
27395
+ snapshot.templateTasks.push(payload);
27140
27396
  else if (row.object_type === "audit_history")
27141
27397
  snapshot.auditHistory.push(payload);
27142
27398
  }
@@ -27354,6 +27610,9 @@ class TodosShadowOutbox {
27354
27610
  case "templates":
27355
27611
  snapshot.templates.push(record);
27356
27612
  break;
27613
+ case "template_tasks":
27614
+ snapshot.templateTasks.push(record);
27615
+ break;
27357
27616
  case "audit_history":
27358
27617
  snapshot.auditHistory.push(record);
27359
27618
  break;
@@ -27420,6 +27679,7 @@ function emptySnapshot() {
27420
27679
  agents: [],
27421
27680
  taskLists: [],
27422
27681
  templates: [],
27682
+ templateTasks: [],
27423
27683
  auditHistory: [],
27424
27684
  tombstones: []
27425
27685
  };
@@ -27616,10 +27876,13 @@ function createPostgresTodosStorageAdapter(options) {
27616
27876
  get: (id) => store.get("templates", id),
27617
27877
  list: async () => (await store.list("templates")).sort((a, b) => a.name.localeCompare(b.name)),
27618
27878
  update: (id, input) => updateTemplate2(id, input, store),
27619
- delete: (id, context) => store.delete("templates", id, context),
27879
+ delete: (id, context) => deleteTemplate2(id, store, context),
27620
27880
  getWithTasks: async (id) => {
27621
27881
  const template = await store.get("templates", id);
27622
- return template ? { ...template, tasks: [] } : null;
27882
+ if (!template)
27883
+ return null;
27884
+ const tasks = (await store.list("template_tasks")).filter((task) => task.template_id === id).sort((left, right) => left.position - right.position || left.id.localeCompare(right.id));
27885
+ return { ...template, tasks };
27623
27886
  }
27624
27887
  },
27625
27888
  audit: {
@@ -27907,6 +28170,57 @@ class PostgresJsonRecordStore {
27907
28170
  }
27908
28171
  return value;
27909
28172
  }
28173
+ async createTemplateWithTasks(template, tasks, context = {}) {
28174
+ await this.ensureSchema();
28175
+ const records = [
28176
+ { object_type: "templates", object_id: template.id, payload: template, updated_at: template.created_at, version: template.version },
28177
+ ...tasks.map((task) => ({ object_type: "template_tasks", object_id: task.id, payload: task, updated_at: task.created_at, version: 1 }))
28178
+ ];
28179
+ const result = await this.options.client.query(`/* todos:create-template-with-tasks-atomic */ WITH input AS (
28180
+ SELECT value->>'object_type' AS object_type,
28181
+ value->>'object_id' AS object_id,
28182
+ value->'payload' AS payload,
28183
+ value->>'updated_at' AS updated_at,
28184
+ COALESCE((value->>'version')::integer, 1) AS version
28185
+ FROM jsonb_array_elements($2::jsonb) AS value
28186
+ ) INSERT INTO ${this.tableName} (
28187
+ service, object_type, object_id, payload, updated_at,
28188
+ deleted_at, source_machine_id, version
28189
+ ) SELECT $1, object_type, object_id, payload, updated_at::timestamptz,
28190
+ NULL, $3, version
28191
+ FROM input
28192
+ ON CONFLICT (service, object_type, object_id) DO UPDATE SET
28193
+ payload = EXCLUDED.payload,
28194
+ updated_at = EXCLUDED.updated_at,
28195
+ deleted_at = NULL,
28196
+ source_machine_id = EXCLUDED.source_machine_id,
28197
+ version = EXCLUDED.version
28198
+ WHERE ${this.tableName}.updated_at IS NULL
28199
+ OR ${this.tableName}.updated_at < EXCLUDED.updated_at
28200
+ OR (${this.tableName}.updated_at = EXCLUDED.updated_at
28201
+ AND COALESCE(${this.tableName}.version, 0) <= COALESCE(EXCLUDED.version, 0))
28202
+ RETURNING object_type, object_id`, [this.service, jsonbParam(records), this.machineId(context)]);
28203
+ if (result.rows.length !== records.length) {
28204
+ throw new Error("Template checklist write was rejected before completion; no partial template was committed");
28205
+ }
28206
+ }
28207
+ async deleteTemplateWithTasks(id, context = {}) {
28208
+ await this.ensureSchema();
28209
+ const timestamp = new Date().toISOString();
28210
+ const result = await this.options.client.query(`/* todos:delete-template-with-tasks-atomic */ WITH target AS (
28211
+ SELECT 1 FROM ${this.tableName}
28212
+ WHERE service = $1 AND object_type = 'templates' AND object_id = $2 AND deleted_at IS NULL
28213
+ ) UPDATE ${this.tableName} AS record SET
28214
+ deleted_at = $3::timestamptz,
28215
+ updated_at = $3::timestamptz,
28216
+ source_machine_id = COALESCE($4, record.source_machine_id),
28217
+ version = COALESCE(record.version, 0) + 1
28218
+ WHERE record.service = $1 AND record.deleted_at IS NULL AND EXISTS (SELECT 1 FROM target)
28219
+ AND (record.object_type = 'templates' AND record.object_id = $2
28220
+ OR record.object_type = 'template_tasks' AND record.payload->>'template_id' = $2)
28221
+ RETURNING record.object_type`, [this.service, id, timestamp, this.machineId(context)]);
28222
+ return result.rows.some((row) => row.object_type === "templates");
28223
+ }
27910
28224
  async completeTask(id, agentId, options) {
27911
28225
  await this.ensureSchema();
27912
28226
  const operationTimestamp = new Date().toISOString();
@@ -28669,7 +28983,7 @@ async function updateTaskList2(id, input, store) {
28669
28983
  }
28670
28984
  async function createTemplate2(input, store, context) {
28671
28985
  const timestamp = new Date().toISOString();
28672
- return store.upsert("templates", {
28986
+ const template = {
28673
28987
  id: randomUUID3(),
28674
28988
  name: input.name,
28675
28989
  title_pattern: input.title_pattern,
@@ -28684,7 +28998,30 @@ async function createTemplate2(input, store, context) {
28684
28998
  created_at: timestamp,
28685
28999
  machine_id: store.machineId(context),
28686
29000
  synced_at: null
28687
- }, context);
29001
+ };
29002
+ const tasks = buildTemplateTasks(template.id, input.tasks ?? [], timestamp);
29003
+ await store.createTemplateWithTasks(template, tasks, context);
29004
+ return template;
29005
+ }
29006
+ function buildTemplateTasks(templateId, inputs, timestamp) {
29007
+ return inputs.map((input, position) => ({
29008
+ id: randomUUID3(),
29009
+ template_id: templateId,
29010
+ position,
29011
+ title_pattern: input.title_pattern,
29012
+ description: input.description ?? null,
29013
+ priority: input.priority ?? "medium",
29014
+ tags: input.tags ?? [],
29015
+ task_type: input.task_type ?? null,
29016
+ condition: input.condition ?? null,
29017
+ include_template_id: input.include_template_id ?? null,
29018
+ depends_on_positions: input.depends_on ?? [],
29019
+ metadata: input.metadata ?? {},
29020
+ created_at: timestamp
29021
+ }));
29022
+ }
29023
+ async function deleteTemplate2(id, store, context) {
29024
+ return store.deleteTemplateWithTasks(id, context);
28688
29025
  }
28689
29026
  async function updateTemplate2(id, input, store) {
28690
29027
  const template = await store.get("templates", id);
@@ -28740,6 +29077,7 @@ async function exportSnapshot(store) {
28740
29077
  agents: await store.list("agents"),
28741
29078
  taskLists: await store.list("task_lists"),
28742
29079
  templates: await store.list("templates"),
29080
+ templateTasks: await store.list("template_tasks"),
28743
29081
  auditHistory: await store.list("audit_history"),
28744
29082
  tombstones: await store.listTombstones()
28745
29083
  };
@@ -28764,6 +29102,7 @@ async function importSnapshot(snapshot, store, context) {
28764
29102
  ...snapshot.agents.map((row) => ["agents", row]),
28765
29103
  ...snapshot.taskLists.map((row) => ["task_lists", row]),
28766
29104
  ...snapshot.templates.map((row) => ["templates", row]),
29105
+ ...(snapshot.templateTasks ?? []).map((row) => ["template_tasks", row]),
28767
29106
  ...snapshot.auditHistory.map((row) => ["audit_history", row])
28768
29107
  ];
28769
29108
  for (const [type, row] of entries) {
@@ -29173,12 +29512,16 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
29173
29512
  TaskList: taskListSchema,
29174
29513
  TaskComment: taskCommentSchema,
29175
29514
  Plan: planSchema,
29515
+ Template: templateSchema,
29516
+ TemplateTask: templateTaskSchema,
29517
+ TemplateVariable: templateVariableSchema,
29518
+ CreateTemplateTaskInput: createTemplateTaskInputSchema,
29176
29519
  CreateTaskInput: {
29177
29520
  type: "object",
29178
29521
  required: ["title"],
29179
29522
  properties: {
29180
29523
  title: { type: "string" },
29181
- description: { type: "string" },
29524
+ description: { type: "string", nullable: true },
29182
29525
  status: { type: "string" },
29183
29526
  priority: { type: "string" },
29184
29527
  project_id: { type: "string" },
@@ -29313,6 +29656,39 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
29313
29656
  agent_id: { type: "string", minLength: 1 },
29314
29657
  status: { type: "string", enum: ["active", "completed", "archived"] }
29315
29658
  }
29659
+ },
29660
+ CreateTemplateInput: {
29661
+ type: "object",
29662
+ additionalProperties: false,
29663
+ required: ["name", "title_pattern"],
29664
+ properties: {
29665
+ name: { type: "string", minLength: 1 },
29666
+ title_pattern: { type: "string", minLength: 1 },
29667
+ description: { type: "string", nullable: true },
29668
+ priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
29669
+ tags: { type: "array", items: { type: "string", minLength: 1 } },
29670
+ variables: { type: "array", items: { $ref: "#/components/schemas/TemplateVariable" } },
29671
+ project_id: { type: "string", minLength: 1, nullable: true },
29672
+ plan_id: { type: "string", minLength: 1, nullable: true },
29673
+ metadata: { type: "object", additionalProperties: true },
29674
+ tasks: { type: "array", items: { $ref: "#/components/schemas/CreateTemplateTaskInput" } }
29675
+ }
29676
+ },
29677
+ UpdateTemplateInput: {
29678
+ type: "object",
29679
+ additionalProperties: false,
29680
+ minProperties: 1,
29681
+ properties: {
29682
+ name: { type: "string", minLength: 1 },
29683
+ title_pattern: { type: "string", minLength: 1 },
29684
+ description: { type: "string", nullable: true },
29685
+ priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
29686
+ tags: { type: "array", items: { type: "string", minLength: 1 } },
29687
+ variables: { type: "array", items: { type: "object" } },
29688
+ project_id: { type: "string", nullable: true },
29689
+ plan_id: { type: "string", nullable: true },
29690
+ metadata: { type: "object", additionalProperties: true }
29691
+ }
29316
29692
  }
29317
29693
  }
29318
29694
  },
@@ -29615,6 +29991,41 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
29615
29991
  responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { deleted: { type: "boolean" }, id: { type: "string" } } } } } } }
29616
29992
  }
29617
29993
  },
29994
+ "/v1/templates": {
29995
+ get: {
29996
+ operationId: "listTemplates",
29997
+ summary: "List reusable task templates",
29998
+ parameters: [{ name: "project_id", in: "query", schema: { type: "string" } }],
29999
+ responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { templates: { type: "array", items: { $ref: "#/components/schemas/Template" } }, count: { type: "number" } } } } } } }
30000
+ },
30001
+ post: {
30002
+ operationId: "createTemplate",
30003
+ summary: "Create a reusable task template",
30004
+ requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/CreateTemplateInput" } } } },
30005
+ responses: { "201": { content: { "application/json": { schema: { type: "object", properties: { template: { $ref: "#/components/schemas/Template" } } } } } } }
30006
+ }
30007
+ },
30008
+ "/v1/templates/{id}": {
30009
+ get: {
30010
+ operationId: "getTemplate",
30011
+ summary: "Get one reusable task template with its checklist steps",
30012
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
30013
+ responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { template: { $ref: "#/components/schemas/Template" } } } } } } }
30014
+ },
30015
+ patch: {
30016
+ operationId: "updateTemplate",
30017
+ summary: "Update reusable template metadata and defaults",
30018
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
30019
+ requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/UpdateTemplateInput" } } } },
30020
+ responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { template: { $ref: "#/components/schemas/Template" } } } } } } }
30021
+ },
30022
+ delete: {
30023
+ operationId: "deleteTemplate",
30024
+ summary: "Delete a reusable task template and its checklist steps",
30025
+ parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
30026
+ responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { deleted: { type: "boolean" }, id: { type: "string" } } } } } } }
30027
+ }
30028
+ },
29618
30029
  "/v1/task-lists": {
29619
30030
  get: {
29620
30031
  operationId: "listTaskLists",
@@ -29692,6 +30103,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
29692
30103
  agents: { type: "array", items: { type: "object" } },
29693
30104
  taskLists: { type: "array", items: { type: "object" } },
29694
30105
  templates: { type: "array", items: { type: "object" } },
30106
+ templateTasks: { type: "array", items: { $ref: "#/components/schemas/TemplateTask" } },
29695
30107
  auditHistory: { type: "array", items: { type: "object" } },
29696
30108
  tombstones: { type: "array", items: { type: "object" } }
29697
30109
  }
@@ -29728,7 +30140,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
29728
30140
  }
29729
30141
  };
29730
30142
  }
29731
- var taskSchema, projectSchema, taskListSchema, taskCommentSchema, planSchema;
30143
+ var taskSchema, projectSchema, taskListSchema, taskCommentSchema, planSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
29732
30144
  var init_openapi = __esm(() => {
29733
30145
  init_package_version();
29734
30146
  taskSchema = {
@@ -29805,6 +30217,72 @@ var init_openapi = __esm(() => {
29805
30217
  updated_at: { type: "string", format: "date-time" }
29806
30218
  }
29807
30219
  };
30220
+ templateTaskSchema = {
30221
+ type: "object",
30222
+ required: ["id", "template_id", "position", "title_pattern", "priority", "tags", "depends_on_positions", "metadata", "created_at"],
30223
+ properties: {
30224
+ id: { type: "string" },
30225
+ template_id: { type: "string" },
30226
+ position: { type: "integer", minimum: 0 },
30227
+ title_pattern: { type: "string" },
30228
+ description: { type: "string", nullable: true },
30229
+ priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
30230
+ tags: { type: "array", items: { type: "string" } },
30231
+ task_type: { type: "string", nullable: true },
30232
+ condition: { type: "string", nullable: true },
30233
+ include_template_id: { type: "string", nullable: true },
30234
+ depends_on_positions: { type: "array", items: { type: "integer", minimum: 0 } },
30235
+ metadata: { type: "object", additionalProperties: true },
30236
+ created_at: { type: "string", format: "date-time" }
30237
+ }
30238
+ };
30239
+ templateSchema = {
30240
+ type: "object",
30241
+ required: ["id", "name", "title_pattern", "priority", "tags", "variables", "version", "metadata", "created_at"],
30242
+ properties: {
30243
+ id: { type: "string" },
30244
+ name: { type: "string" },
30245
+ title_pattern: { type: "string" },
30246
+ description: { type: "string", nullable: true },
30247
+ priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
30248
+ tags: { type: "array", items: { type: "string" } },
30249
+ variables: { type: "array", items: { type: "object", properties: { name: { type: "string" }, required: { type: "boolean" }, default: { type: "string" }, description: { type: "string" } } } },
30250
+ version: { type: "integer", minimum: 1 },
30251
+ project_id: { type: "string", nullable: true },
30252
+ plan_id: { type: "string", nullable: true },
30253
+ metadata: { type: "object", additionalProperties: true },
30254
+ created_at: { type: "string", format: "date-time" },
30255
+ tasks: { type: "array", items: { $ref: "#/components/schemas/TemplateTask" } }
30256
+ }
30257
+ };
30258
+ templateVariableSchema = {
30259
+ type: "object",
30260
+ required: ["name", "required"],
30261
+ properties: {
30262
+ name: { type: "string" },
30263
+ required: { type: "boolean" },
30264
+ default: { type: "string" },
30265
+ description: { type: "string" }
30266
+ }
30267
+ };
30268
+ createTemplateTaskInputSchema = {
30269
+ type: "object",
30270
+ additionalProperties: false,
30271
+ required: ["title_pattern"],
30272
+ properties: {
30273
+ position: { type: "integer", minimum: 0 },
30274
+ title_pattern: { type: "string", minLength: 1 },
30275
+ description: { type: "string", nullable: true },
30276
+ priority: { type: "string", enum: ["low", "medium", "high", "critical"] },
30277
+ tags: { type: "array", items: { type: "string", minLength: 1 } },
30278
+ task_type: { type: "string", nullable: true },
30279
+ condition: { type: "string", nullable: true },
30280
+ include_template_id: { type: "string", nullable: true },
30281
+ depends_on: { type: "array", items: { type: "integer", minimum: 0 } },
30282
+ depends_on_positions: { type: "array", items: { type: "integer", minimum: 0 } },
30283
+ metadata: { type: "object", additionalProperties: true }
30284
+ }
30285
+ };
29808
30286
  });
29809
30287
 
29810
30288
  // src/server/v1.ts
@@ -29944,6 +30422,122 @@ function validatePlanCreate(value) {
29944
30422
  }
29945
30423
  };
29946
30424
  }
30425
+ function validateTemplateTask(value) {
30426
+ if (!value || typeof value !== "object" || Array.isArray(value))
30427
+ return null;
30428
+ const body = value;
30429
+ const allowed = new Set(["position", "title_pattern", "description", "priority", "tags", "task_type", "condition", "include_template_id", "depends_on", "depends_on_positions", "metadata"]);
30430
+ if (Object.keys(body).some((key) => !allowed.has(key)))
30431
+ return null;
30432
+ if (typeof body.title_pattern !== "string" || !body.title_pattern.trim())
30433
+ return null;
30434
+ if (body.position !== undefined && (typeof body.position !== "number" || !Number.isSafeInteger(body.position) || body.position < 0))
30435
+ return null;
30436
+ if (body.description !== undefined && body.description !== null && typeof body.description !== "string")
30437
+ return null;
30438
+ if (body.priority !== undefined && (typeof body.priority !== "string" || !["low", "medium", "high", "critical"].includes(body.priority)))
30439
+ return null;
30440
+ if (body.tags !== undefined && (!Array.isArray(body.tags) || body.tags.some((tag) => typeof tag !== "string" || !tag.trim())))
30441
+ return null;
30442
+ for (const field of ["task_type", "condition", "include_template_id"]) {
30443
+ if (body[field] !== undefined && body[field] !== null && (typeof body[field] !== "string" || !body[field].trim()))
30444
+ return null;
30445
+ }
30446
+ if (body.depends_on !== undefined && body.depends_on_positions !== undefined)
30447
+ return null;
30448
+ const dependencies = body.depends_on ?? body.depends_on_positions;
30449
+ if (dependencies !== undefined && (!Array.isArray(dependencies) || dependencies.some((position) => !Number.isSafeInteger(position) || position < 0)))
30450
+ return null;
30451
+ if (body.metadata !== undefined && (!body.metadata || typeof body.metadata !== "object" || Array.isArray(body.metadata)))
30452
+ return null;
30453
+ return {
30454
+ title_pattern: body.title_pattern,
30455
+ ...typeof body.description === "string" ? { description: body.description } : {},
30456
+ ...typeof body.priority === "string" ? { priority: body.priority } : {},
30457
+ ...Array.isArray(body.tags) ? { tags: body.tags } : {},
30458
+ ...typeof body.task_type === "string" ? { task_type: body.task_type } : {},
30459
+ ...typeof body.condition === "string" ? { condition: body.condition } : {},
30460
+ ...typeof body.include_template_id === "string" ? { include_template_id: body.include_template_id } : {},
30461
+ ...Array.isArray(dependencies) ? { depends_on: dependencies } : {},
30462
+ ...body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata) ? { metadata: body.metadata } : {}
30463
+ };
30464
+ }
30465
+ function validateTemplateCreate(value) {
30466
+ if (!value || typeof value !== "object" || Array.isArray(value))
30467
+ return { ok: false, message: "template body must be an object" };
30468
+ const body = value;
30469
+ const allowed = new Set(["name", "title_pattern", "description", "priority", "tags", "variables", "project_id", "plan_id", "metadata", "tasks"]);
30470
+ const unknown = Object.keys(body).find((key) => !allowed.has(key));
30471
+ if (unknown)
30472
+ return { ok: false, message: `unknown template field: ${unknown}` };
30473
+ if (typeof body.name !== "string" || !body.name.trim())
30474
+ return { ok: false, message: "name must be a non-empty string" };
30475
+ if (typeof body.title_pattern !== "string" || !body.title_pattern.trim())
30476
+ return { ok: false, message: "title_pattern must be a non-empty string" };
30477
+ if (body.description !== undefined && body.description !== null && typeof body.description !== "string")
30478
+ return { ok: false, message: "description must be a string or null" };
30479
+ if (body.priority !== undefined && (typeof body.priority !== "string" || !["low", "medium", "high", "critical"].includes(body.priority)))
30480
+ return { ok: false, message: "priority must be low, medium, high, or critical" };
30481
+ if (body.tags !== undefined && (!Array.isArray(body.tags) || body.tags.some((tag) => typeof tag !== "string" || !tag.trim())))
30482
+ return { ok: false, message: "tags must be an array of non-empty strings" };
30483
+ 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"))) {
30484
+ return { ok: false, message: "variables must be valid template variable objects" };
30485
+ }
30486
+ for (const field of ["project_id", "plan_id"]) {
30487
+ if (body[field] !== undefined && body[field] !== null && (typeof body[field] !== "string" || !body[field].trim()))
30488
+ return { ok: false, message: `${field} must be a non-empty string or null` };
30489
+ }
30490
+ if (body.metadata !== undefined && (!body.metadata || typeof body.metadata !== "object" || Array.isArray(body.metadata)))
30491
+ return { ok: false, message: "metadata must be an object" };
30492
+ const tasks = body.tasks === undefined ? [] : Array.isArray(body.tasks) ? body.tasks.map(validateTemplateTask) : null;
30493
+ if (tasks === null || tasks.some((task) => task === null))
30494
+ return { ok: false, message: "tasks must be valid template task objects" };
30495
+ const taskInputs = tasks;
30496
+ for (const [position, task] of taskInputs.entries()) {
30497
+ if ((task.depends_on ?? []).some((dependency) => dependency >= position)) {
30498
+ return { ok: false, message: "template task dependencies must reference earlier task positions" };
30499
+ }
30500
+ }
30501
+ return {
30502
+ ok: true,
30503
+ input: {
30504
+ name: body.name,
30505
+ title_pattern: body.title_pattern,
30506
+ ...typeof body.description === "string" ? { description: body.description } : {},
30507
+ ...typeof body.priority === "string" ? { priority: body.priority } : {},
30508
+ ...Array.isArray(body.tags) ? { tags: body.tags } : {},
30509
+ ...Array.isArray(body.variables) ? { variables: body.variables } : {},
30510
+ ...typeof body.project_id === "string" ? { project_id: body.project_id } : {},
30511
+ ...typeof body.plan_id === "string" ? { plan_id: body.plan_id } : {},
30512
+ ...body.metadata && typeof body.metadata === "object" && !Array.isArray(body.metadata) ? { metadata: body.metadata } : {},
30513
+ tasks: taskInputs
30514
+ }
30515
+ };
30516
+ }
30517
+ function validateTemplatePatch(value) {
30518
+ if (!value || typeof value !== "object" || Array.isArray(value))
30519
+ return { ok: false, message: "template patch must be an object" };
30520
+ const body = value;
30521
+ const allowed = new Set(["name", "title_pattern", "description", "priority", "tags", "variables", "project_id", "plan_id", "metadata"]);
30522
+ const unknown = Object.keys(body).find((key) => !allowed.has(key));
30523
+ if (unknown)
30524
+ return { ok: false, message: `unknown template field: ${unknown}` };
30525
+ if (Object.keys(body).length === 0)
30526
+ return { ok: false, message: "template patch must not be empty" };
30527
+ const templateLike = { name: body.name ?? "template", title_pattern: body.title_pattern ?? "template", ...body };
30528
+ const validated = validateTemplateCreate(templateLike);
30529
+ if (!validated.ok)
30530
+ return validated;
30531
+ const { name: _name, title_pattern: _title, tasks: _tasks, ...patch } = validated.input;
30532
+ return { ok: true, patch: {
30533
+ ...body.name !== undefined ? { name: validated.input.name } : {},
30534
+ ...body.title_pattern !== undefined ? { title_pattern: validated.input.title_pattern } : {},
30535
+ ...patch,
30536
+ ...body.description === null ? { description: null } : {},
30537
+ ...body.project_id === null ? { project_id: null } : {},
30538
+ ...body.plan_id === null ? { plan_id: null } : {}
30539
+ } };
30540
+ }
29947
30541
  async function readJson(req) {
29948
30542
  try {
29949
30543
  const text = await req.text();
@@ -30004,12 +30598,13 @@ function normalizeImportSnapshot(raw) {
30004
30598
  agents: arr(body["agents"]),
30005
30599
  taskLists: arr(body["taskLists"]),
30006
30600
  templates: arr(body["templates"]),
30601
+ templateTasks: arr(body["templateTasks"]),
30007
30602
  auditHistory: arr(body["auditHistory"]),
30008
30603
  tombstones: arr(body["tombstones"])
30009
30604
  };
30010
30605
  }
30011
30606
  function countSnapshotRecords(s) {
30012
- 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);
30607
+ 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);
30013
30608
  }
30014
30609
  async function handleV1Request(req, url, dependencies = {}) {
30015
30610
  const path = url.pathname;
@@ -30579,6 +31174,40 @@ async function handleV1Request(req, url, dependencies = {}) {
30579
31174
  if (id)
30580
31175
  return error(405, `method ${method} not allowed on /v1/plans/:id`);
30581
31176
  }
31177
+ if (resource === "templates") {
31178
+ if (!id && method === "GET") {
31179
+ const projectId = url.searchParams.get("project_id");
31180
+ const templates = (await store.templates.list()).filter((template) => projectId === null || template.project_id === projectId);
31181
+ return json2({ templates, count: templates.length });
31182
+ }
31183
+ if (!id && method === "POST") {
31184
+ const body = await readJson(req);
31185
+ const validated = validateTemplateCreate(body);
31186
+ if (!validated.ok)
31187
+ return error(400, validated.message);
31188
+ const template = await store.templates.create(validated.input, contextFromPrincipal(principal));
31189
+ return json2({ template: await store.templates.getWithTasks(template.id) }, 201);
31190
+ }
31191
+ if (!id)
31192
+ return error(405, `method ${method} not allowed on /v1/templates`);
31193
+ if (method === "GET") {
31194
+ const template = await store.templates.getWithTasks(id);
31195
+ return template ? json2({ template }) : error(404, "template not found");
31196
+ }
31197
+ if (method === "PATCH" || method === "PUT") {
31198
+ const body = await readJson(req);
31199
+ const validated = validateTemplatePatch(body);
31200
+ if (!validated.ok)
31201
+ return error(400, validated.message);
31202
+ const template = await store.templates.update(id, validated.patch, contextFromPrincipal(principal));
31203
+ return template ? json2({ template: await store.templates.getWithTasks(id) }) : error(404, "template not found");
31204
+ }
31205
+ if (method === "DELETE") {
31206
+ const deleted = await store.templates.delete(id, contextFromPrincipal(principal));
31207
+ return deleted ? json2({ deleted: true, id }) : error(404, "template not found");
31208
+ }
31209
+ return error(405, `method ${method} not allowed on /v1/templates/:id`);
31210
+ }
30582
31211
  if (resource === "agents") {
30583
31212
  if (!id && method === "GET") {
30584
31213
  const agents = await store.agents.list();
@@ -53808,9 +54437,9 @@ ${task2.id.slice(0, 8)} | ${task2.priority} | ${task2.title}` }] };
53808
54437
  if (shouldRegisterTool("delete_template")) {
53809
54438
  server.tool("delete_template", "Delete a task template by ID.", { id: exports_external.string() }, async ({ id }) => {
53810
54439
  try {
53811
- const { deleteTemplate: deleteTemplate2 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
54440
+ const { deleteTemplate: deleteTemplate3 } = await Promise.resolve().then(() => (init_templates(), exports_templates));
53812
54441
  const resolvedId = resolveId(id, "task_templates");
53813
- const deleted = deleteTemplate2(resolvedId);
54442
+ const deleted = deleteTemplate3(resolvedId);
53814
54443
  return { content: [{ type: "text", text: deleted ? "Template deleted." : "Template not found." }] };
53815
54444
  } catch (e) {
53816
54445
  return { content: [{ type: "text", text: formatError(e) }], isError: true };
@@ -69258,6 +69887,7 @@ function emptySnapshot2() {
69258
69887
  agents: [],
69259
69888
  taskLists: [],
69260
69889
  templates: [],
69890
+ templateTasks: [],
69261
69891
  auditHistory: [],
69262
69892
  tombstones: []
69263
69893
  };
@@ -69446,6 +70076,7 @@ var init_shadow = __esm(() => {
69446
70076
  agents: "agents",
69447
70077
  taskLists: "task_lists",
69448
70078
  templates: "templates",
70079
+ templateTasks: "template_tasks",
69449
70080
  auditHistory: "audit_history"
69450
70081
  };
69451
70082
  });
@@ -71476,6 +72107,8 @@ var REMOTE_COMMANDS = new Set([
71476
72107
  "task",
71477
72108
  "task-lists",
71478
72109
  "timeline",
72110
+ "template-import",
72111
+ "templates",
71479
72112
  "tl",
71480
72113
  "unlock",
71481
72114
  "update"