@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/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();
@@ -30787,7 +30793,7 @@ var package_default;
30787
30793
  var init_package = __esm(() => {
30788
30794
  package_default = {
30789
30795
  name: "@hasna/todos",
30790
- version: "0.11.94",
30796
+ version: "0.11.95",
30791
30797
  description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
30792
30798
  type: "module",
30793
30799
  main: "dist/index.js",
@@ -40131,6 +40137,7 @@ function exportSqliteTodosStorageSnapshot(db) {
40131
40137
  agents: listAgents({ include_archived: true }, d),
40132
40138
  taskLists: listTaskLists(undefined, d),
40133
40139
  templates: listTemplates(d),
40140
+ templateTasks: listTemplates(d).flatMap((template) => getTemplateTasks(template.id, d)),
40134
40141
  auditHistory: getRecentActivity(Number.MAX_SAFE_INTEGER, d),
40135
40142
  tombstones: listStorageTombstones(d)
40136
40143
  };
@@ -40180,6 +40187,7 @@ function importSqliteTodosStorageSnapshot(snapshot, db) {
40180
40187
  applyRows("task_lists", "task_lists", TASK_LIST_COLUMNS, snapshot.taskLists, "updated_at");
40181
40188
  applyRows("plans", "plans", PLAN_COLUMNS, snapshot.plans, "updated_at");
40182
40189
  applyRows("templates", "task_templates", TEMPLATE_COLUMNS, snapshot.templates);
40190
+ applyRows("template_tasks", "template_tasks", TEMPLATE_TASK_COLUMNS, snapshot.templateTasks ?? []);
40183
40191
  applyRows("tasks", "tasks", TASK_COLUMNS, sortedTasks2(snapshot.tasks), "updated_at", (row, changed) => {
40184
40192
  if (changed && Array.isArray(row["tags"]) && typeof row["id"] === "string") {
40185
40193
  replaceTaskTags(row["id"], row["tags"].filter((tag) => typeof tag === "string"), d);
@@ -40284,6 +40292,8 @@ function tableForTombstone(objectType2) {
40284
40292
  return "task_lists";
40285
40293
  if (objectType2 === "templates")
40286
40294
  return "task_templates";
40295
+ if (objectType2 === "template_tasks")
40296
+ return "template_tasks";
40287
40297
  return "task_history";
40288
40298
  }
40289
40299
  function listRows(db, table, columns) {
@@ -40320,7 +40330,7 @@ function clockColumnsForTable(table) {
40320
40330
  return ["created_at"];
40321
40331
  return ["updated_at", "created_at"];
40322
40332
  }
40323
- 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;
40324
40334
  var init_sqlite_snapshot = __esm(() => {
40325
40335
  init_database();
40326
40336
  init_agents();
@@ -40414,6 +40424,21 @@ var init_sqlite_snapshot = __esm(() => {
40414
40424
  "machine_id",
40415
40425
  "synced_at"
40416
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
+ ];
40417
40442
  TASK_COLUMNS = [
40418
40443
  "id",
40419
40444
  "short_id",
@@ -40482,7 +40507,7 @@ var init_sqlite_snapshot = __esm(() => {
40482
40507
  "created_at",
40483
40508
  "machine_id"
40484
40509
  ];
40485
- JSON_COLUMNS = new Set(["tags", "metadata", "permissions", "capabilities", "variables"]);
40510
+ JSON_COLUMNS = new Set(["tags", "metadata", "permissions", "capabilities", "variables", "depends_on_positions"]);
40486
40511
  BOOLEAN_COLUMNS = new Set(["requires_approval"]);
40487
40512
  });
40488
40513
 
@@ -40872,6 +40897,7 @@ function snapshotEntries(snapshot) {
40872
40897
  ...snapshot.agents.map((payload) => entry("agents", payload, snapshot.exportedAt)),
40873
40898
  ...snapshot.taskLists.map((payload) => entry("task_lists", payload, snapshot.exportedAt)),
40874
40899
  ...snapshot.templates.map((payload) => entry("templates", payload, snapshot.exportedAt)),
40900
+ ...(snapshot.templateTasks ?? []).map((payload) => entry("template_tasks", payload, snapshot.exportedAt)),
40875
40901
  ...snapshot.auditHistory.map((payload) => entry("audit_history", payload, snapshot.exportedAt)),
40876
40902
  ...(snapshot.tombstones ?? []).map((tombstone) => ({
40877
40903
  type: tombstone.object_type,
@@ -40923,6 +40949,7 @@ function rowsToSnapshot(rows) {
40923
40949
  agents: [],
40924
40950
  taskLists: [],
40925
40951
  templates: [],
40952
+ templateTasks: [],
40926
40953
  auditHistory: [],
40927
40954
  tombstones: []
40928
40955
  };
@@ -40957,6 +40984,8 @@ function rowsToSnapshot(rows) {
40957
40984
  snapshot.taskLists.push(payload);
40958
40985
  else if (row.object_type === "templates")
40959
40986
  snapshot.templates.push(payload);
40987
+ else if (row.object_type === "template_tasks")
40988
+ snapshot.templateTasks.push(payload);
40960
40989
  else if (row.object_type === "audit_history")
40961
40990
  snapshot.auditHistory.push(payload);
40962
40991
  }
@@ -41174,6 +41203,9 @@ class TodosShadowOutbox {
41174
41203
  case "templates":
41175
41204
  snapshot.templates.push(record);
41176
41205
  break;
41206
+ case "template_tasks":
41207
+ snapshot.templateTasks.push(record);
41208
+ break;
41177
41209
  case "audit_history":
41178
41210
  snapshot.auditHistory.push(record);
41179
41211
  break;
@@ -41240,6 +41272,7 @@ function emptySnapshot() {
41240
41272
  agents: [],
41241
41273
  taskLists: [],
41242
41274
  templates: [],
41275
+ templateTasks: [],
41243
41276
  auditHistory: [],
41244
41277
  tombstones: []
41245
41278
  };
@@ -42439,10 +42472,13 @@ function createPostgresTodosStorageAdapter(options) {
42439
42472
  get: (id) => store.get("templates", id),
42440
42473
  list: async () => (await store.list("templates")).sort((a, b) => a.name.localeCompare(b.name)),
42441
42474
  update: (id, input) => updateTemplate2(id, input, store),
42442
- delete: (id, context) => store.delete("templates", id, context),
42475
+ delete: (id, context) => deleteTemplate2(id, store, context),
42443
42476
  getWithTasks: async (id) => {
42444
42477
  const template = await store.get("templates", id);
42445
- 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 };
42446
42482
  }
42447
42483
  },
42448
42484
  audit: {
@@ -42730,6 +42766,57 @@ class PostgresJsonRecordStore {
42730
42766
  }
42731
42767
  return value;
42732
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
+ }
42733
42820
  async completeTask(id, agentId, options) {
42734
42821
  await this.ensureSchema();
42735
42822
  const operationTimestamp = new Date().toISOString();
@@ -43492,7 +43579,7 @@ async function updateTaskList2(id, input, store) {
43492
43579
  }
43493
43580
  async function createTemplate2(input, store, context) {
43494
43581
  const timestamp3 = new Date().toISOString();
43495
- return store.upsert("templates", {
43582
+ const template = {
43496
43583
  id: randomUUID3(),
43497
43584
  name: input.name,
43498
43585
  title_pattern: input.title_pattern,
@@ -43507,7 +43594,30 @@ async function createTemplate2(input, store, context) {
43507
43594
  created_at: timestamp3,
43508
43595
  machine_id: store.machineId(context),
43509
43596
  synced_at: null
43510
- }, 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);
43511
43621
  }
43512
43622
  async function updateTemplate2(id, input, store) {
43513
43623
  const template = await store.get("templates", id);
@@ -43563,6 +43673,7 @@ async function exportSnapshot(store) {
43563
43673
  agents: await store.list("agents"),
43564
43674
  taskLists: await store.list("task_lists"),
43565
43675
  templates: await store.list("templates"),
43676
+ templateTasks: await store.list("template_tasks"),
43566
43677
  auditHistory: await store.list("audit_history"),
43567
43678
  tombstones: await store.listTombstones()
43568
43679
  };
@@ -43587,6 +43698,7 @@ async function importSnapshot(snapshot, store, context) {
43587
43698
  ...snapshot.agents.map((row) => ["agents", row]),
43588
43699
  ...snapshot.taskLists.map((row) => ["task_lists", row]),
43589
43700
  ...snapshot.templates.map((row) => ["templates", row]),
43701
+ ...(snapshot.templateTasks ?? []).map((row) => ["template_tasks", row]),
43590
43702
  ...snapshot.auditHistory.map((row) => ["audit_history", row])
43591
43703
  ];
43592
43704
  for (const [type, row] of entries) {
@@ -43993,12 +44105,16 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
43993
44105
  TaskList: taskListSchema,
43994
44106
  TaskComment: taskCommentSchema,
43995
44107
  Plan: planSchema,
44108
+ Template: templateSchema,
44109
+ TemplateTask: templateTaskSchema,
44110
+ TemplateVariable: templateVariableSchema,
44111
+ CreateTemplateTaskInput: createTemplateTaskInputSchema,
43996
44112
  CreateTaskInput: {
43997
44113
  type: "object",
43998
44114
  required: ["title"],
43999
44115
  properties: {
44000
44116
  title: { type: "string" },
44001
- description: { type: "string" },
44117
+ description: { type: "string", nullable: true },
44002
44118
  status: { type: "string" },
44003
44119
  priority: { type: "string" },
44004
44120
  project_id: { type: "string" },
@@ -44133,6 +44249,39 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
44133
44249
  agent_id: { type: "string", minLength: 1 },
44134
44250
  status: { type: "string", enum: ["active", "completed", "archived"] }
44135
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
+ }
44136
44285
  }
44137
44286
  }
44138
44287
  },
@@ -44435,6 +44584,41 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
44435
44584
  responses: { "200": { content: { "application/json": { schema: { type: "object", properties: { deleted: { type: "boolean" }, id: { type: "string" } } } } } } }
44436
44585
  }
44437
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
+ },
44438
44622
  "/v1/task-lists": {
44439
44623
  get: {
44440
44624
  operationId: "listTaskLists",
@@ -44512,6 +44696,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
44512
44696
  agents: { type: "array", items: { type: "object" } },
44513
44697
  taskLists: { type: "array", items: { type: "object" } },
44514
44698
  templates: { type: "array", items: { type: "object" } },
44699
+ templateTasks: { type: "array", items: { $ref: "#/components/schemas/TemplateTask" } },
44515
44700
  auditHistory: { type: "array", items: { type: "object" } },
44516
44701
  tombstones: { type: "array", items: { type: "object" } }
44517
44702
  }
@@ -44548,7 +44733,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
44548
44733
  }
44549
44734
  };
44550
44735
  }
44551
- var taskSchema, projectSchema, taskListSchema, taskCommentSchema, planSchema;
44736
+ var taskSchema, projectSchema, taskListSchema, taskCommentSchema, planSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
44552
44737
  var init_openapi = __esm(() => {
44553
44738
  init_package_version();
44554
44739
  taskSchema = {
@@ -44625,6 +44810,72 @@ var init_openapi = __esm(() => {
44625
44810
  updated_at: { type: "string", format: "date-time" }
44626
44811
  }
44627
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
+ };
44628
44879
  });
44629
44880
 
44630
44881
  // src/server/v1.ts
@@ -44764,6 +45015,122 @@ function validatePlanCreate(value) {
44764
45015
  }
44765
45016
  };
44766
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
+ }
44767
45134
  async function readJson(req) {
44768
45135
  try {
44769
45136
  const text2 = await req.text();
@@ -44824,12 +45191,13 @@ function normalizeImportSnapshot(raw) {
44824
45191
  agents: arr(body["agents"]),
44825
45192
  taskLists: arr(body["taskLists"]),
44826
45193
  templates: arr(body["templates"]),
45194
+ templateTasks: arr(body["templateTasks"]),
44827
45195
  auditHistory: arr(body["auditHistory"]),
44828
45196
  tombstones: arr(body["tombstones"])
44829
45197
  };
44830
45198
  }
44831
45199
  function countSnapshotRecords(s) {
44832
- 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);
44833
45201
  }
44834
45202
  async function handleV1Request(req, url, dependencies = {}) {
44835
45203
  const path = url.pathname;
@@ -45399,6 +45767,40 @@ async function handleV1Request(req, url, dependencies = {}) {
45399
45767
  if (id)
45400
45768
  return error(405, `method ${method} not allowed on /v1/plans/:id`);
45401
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
+ }
45402
45804
  if (resource === "agents") {
45403
45805
  if (!id && method === "GET") {
45404
45806
  const agents = await store.agents.list();