@hasna/todos 0.11.10 → 0.11.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -846,6 +846,26 @@ var MIGRATIONS = [
846
846
  CREATE INDEX IF NOT EXISTS idx_template_tasks_template ON template_tasks(template_id);
847
847
 
848
848
  INSERT OR IGNORE INTO _migrations (id) VALUES (37);
849
+ `,
850
+ `
851
+ ALTER TABLE task_templates ADD COLUMN variables TEXT DEFAULT '[]';
852
+ INSERT OR IGNORE INTO _migrations (id) VALUES (38);
853
+ `,
854
+ `
855
+ ALTER TABLE template_tasks ADD COLUMN condition TEXT;
856
+ ALTER TABLE template_tasks ADD COLUMN include_template_id TEXT;
857
+ ALTER TABLE task_templates ADD COLUMN version INTEGER NOT NULL DEFAULT 1;
858
+
859
+ CREATE TABLE IF NOT EXISTS template_versions (
860
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
861
+ template_id TEXT NOT NULL REFERENCES task_templates(id) ON DELETE CASCADE,
862
+ version INTEGER NOT NULL,
863
+ snapshot TEXT NOT NULL,
864
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
865
+ );
866
+ CREATE INDEX IF NOT EXISTS idx_template_versions_template ON template_versions(template_id);
867
+
868
+ INSERT OR IGNORE INTO _migrations (id) VALUES (39);
849
869
  `
850
870
  ];
851
871
  var _db = null;
@@ -1060,6 +1080,36 @@ function ensureSchema(db) {
1060
1080
  ensureColumn("projects", "org_id", "TEXT");
1061
1081
  ensureColumn("plans", "task_list_id", "TEXT");
1062
1082
  ensureColumn("plans", "agent_id", "TEXT");
1083
+ ensureColumn("task_templates", "variables", "TEXT DEFAULT '[]'");
1084
+ ensureColumn("task_templates", "version", "INTEGER NOT NULL DEFAULT 1");
1085
+ ensureColumn("template_tasks", "condition", "TEXT");
1086
+ ensureColumn("template_tasks", "include_template_id", "TEXT");
1087
+ ensureTable("template_versions", `
1088
+ CREATE TABLE template_versions (
1089
+ id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
1090
+ template_id TEXT NOT NULL REFERENCES task_templates(id) ON DELETE CASCADE,
1091
+ version INTEGER NOT NULL,
1092
+ snapshot TEXT NOT NULL,
1093
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
1094
+ )`);
1095
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_template_versions_template ON template_versions(template_id)");
1096
+ ensureColumn("webhooks", "project_id", "TEXT");
1097
+ ensureColumn("webhooks", "task_list_id", "TEXT");
1098
+ ensureColumn("webhooks", "agent_id", "TEXT");
1099
+ ensureColumn("webhooks", "task_id", "TEXT");
1100
+ ensureTable("webhook_deliveries", `
1101
+ CREATE TABLE webhook_deliveries (
1102
+ id TEXT PRIMARY KEY,
1103
+ webhook_id TEXT NOT NULL REFERENCES webhooks(id) ON DELETE CASCADE,
1104
+ event TEXT NOT NULL,
1105
+ payload TEXT NOT NULL,
1106
+ status_code INTEGER,
1107
+ response TEXT,
1108
+ attempt INTEGER NOT NULL DEFAULT 1,
1109
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
1110
+ )`);
1111
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_webhook ON webhook_deliveries(webhook_id)");
1112
+ ensureIndex("CREATE INDEX IF NOT EXISTS idx_webhook_deliveries_event ON webhook_deliveries(event)");
1063
1113
  ensureColumn("task_comments", "type", "TEXT DEFAULT 'comment'");
1064
1114
  ensureColumn("task_comments", "progress_pct", "INTEGER");
1065
1115
  ensureIndex("CREATE INDEX IF NOT EXISTS idx_tasks_plan ON tasks(plan_id)");
@@ -1750,13 +1800,33 @@ function nextOccurrence(rule, from) {
1750
1800
  }
1751
1801
 
1752
1802
  // src/db/webhooks.ts
1803
+ var MAX_RETRY_ATTEMPTS = 3;
1804
+ var RETRY_BASE_DELAY_MS = 1000;
1753
1805
  function rowToWebhook(row) {
1754
- return { ...row, events: JSON.parse(row.events || "[]"), active: !!row.active };
1806
+ return {
1807
+ ...row,
1808
+ events: JSON.parse(row.events || "[]"),
1809
+ active: !!row.active,
1810
+ project_id: row.project_id || null,
1811
+ task_list_id: row.task_list_id || null,
1812
+ agent_id: row.agent_id || null,
1813
+ task_id: row.task_id || null
1814
+ };
1755
1815
  }
1756
1816
  function createWebhook(input, db) {
1757
1817
  const d = db || getDatabase();
1758
1818
  const id = uuid();
1759
- d.run(`INSERT INTO webhooks (id, url, events, secret, created_at) VALUES (?, ?, ?, ?, ?)`, [id, input.url, JSON.stringify(input.events || []), input.secret || null, now()]);
1819
+ d.run(`INSERT INTO webhooks (id, url, events, secret, project_id, task_list_id, agent_id, task_id, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1820
+ id,
1821
+ input.url,
1822
+ JSON.stringify(input.events || []),
1823
+ input.secret || null,
1824
+ input.project_id || null,
1825
+ input.task_list_id || null,
1826
+ input.agent_id || null,
1827
+ input.task_id || null,
1828
+ now()
1829
+ ]);
1760
1830
  return getWebhook(id, d);
1761
1831
  }
1762
1832
  function getWebhook(id, db) {
@@ -1772,20 +1842,66 @@ function deleteWebhook(id, db) {
1772
1842
  const d = db || getDatabase();
1773
1843
  return d.run("DELETE FROM webhooks WHERE id = ?", [id]).changes > 0;
1774
1844
  }
1845
+ function listDeliveries(webhookId, limit = 50, db) {
1846
+ const d = db || getDatabase();
1847
+ if (webhookId) {
1848
+ return d.query("SELECT * FROM webhook_deliveries WHERE webhook_id = ? ORDER BY created_at DESC LIMIT ?").all(webhookId, limit);
1849
+ }
1850
+ return d.query("SELECT * FROM webhook_deliveries ORDER BY created_at DESC LIMIT ?").all(limit);
1851
+ }
1852
+ function logDelivery(d, webhookId, event, payload, statusCode, response, attempt) {
1853
+ const id = uuid();
1854
+ d.run(`INSERT INTO webhook_deliveries (id, webhook_id, event, payload, status_code, response, attempt, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [id, webhookId, event, payload, statusCode, response, attempt, now()]);
1855
+ }
1856
+ function matchesScope(wh, payload) {
1857
+ if (wh.project_id && payload.project_id !== wh.project_id)
1858
+ return false;
1859
+ if (wh.task_list_id && payload.task_list_id !== wh.task_list_id)
1860
+ return false;
1861
+ if (wh.agent_id && payload.agent_id !== wh.agent_id && payload.assigned_to !== wh.agent_id)
1862
+ return false;
1863
+ if (wh.task_id && payload.id !== wh.task_id)
1864
+ return false;
1865
+ return true;
1866
+ }
1867
+ async function deliverWebhook(wh, event, body, attempt, db) {
1868
+ try {
1869
+ const headers = { "Content-Type": "application/json" };
1870
+ if (wh.secret) {
1871
+ const encoder = new TextEncoder;
1872
+ const key = await crypto.subtle.importKey("raw", encoder.encode(wh.secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
1873
+ const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
1874
+ headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
1875
+ }
1876
+ const resp = await fetch(wh.url, { method: "POST", headers, body });
1877
+ const respText = await resp.text().catch(() => "");
1878
+ logDelivery(db, wh.id, event, body, resp.status, respText.slice(0, 1000), attempt);
1879
+ if (resp.status >= 400 && attempt < MAX_RETRY_ATTEMPTS) {
1880
+ const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
1881
+ setTimeout(() => {
1882
+ deliverWebhook(wh, event, body, attempt + 1, db).catch(() => {});
1883
+ }, delay);
1884
+ }
1885
+ } catch (err) {
1886
+ const errorMsg = err instanceof Error ? err.message : String(err);
1887
+ logDelivery(db, wh.id, event, body, null, errorMsg.slice(0, 1000), attempt);
1888
+ if (attempt < MAX_RETRY_ATTEMPTS) {
1889
+ const delay = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 1);
1890
+ setTimeout(() => {
1891
+ deliverWebhook(wh, event, body, attempt + 1, db).catch(() => {});
1892
+ }, delay);
1893
+ }
1894
+ }
1895
+ }
1775
1896
  async function dispatchWebhook(event, payload, db) {
1776
- const webhooks = listWebhooks(db).filter((w) => w.active && (w.events.length === 0 || w.events.includes(event)));
1897
+ const d = db || getDatabase();
1898
+ const webhooks = listWebhooks(d).filter((w) => w.active && (w.events.length === 0 || w.events.includes(event)));
1899
+ const payloadObj = typeof payload === "object" && payload !== null ? payload : {};
1777
1900
  for (const wh of webhooks) {
1778
- try {
1779
- const body = JSON.stringify({ event, payload, timestamp: now() });
1780
- const headers = { "Content-Type": "application/json" };
1781
- if (wh.secret) {
1782
- const encoder = new TextEncoder;
1783
- const key = await crypto.subtle.importKey("raw", encoder.encode(wh.secret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
1784
- const sig = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
1785
- headers["X-Webhook-Signature"] = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
1786
- }
1787
- fetch(wh.url, { method: "POST", headers, body }).catch(() => {});
1788
- } catch {}
1901
+ if (!matchesScope(wh, payloadObj))
1902
+ continue;
1903
+ const body = JSON.stringify({ event, payload, timestamp: now() });
1904
+ deliverWebhook(wh, event, body, 1, d).catch(() => {});
1789
1905
  }
1790
1906
  }
1791
1907
 
@@ -1794,8 +1910,10 @@ function rowToTemplate(row) {
1794
1910
  return {
1795
1911
  ...row,
1796
1912
  tags: JSON.parse(row.tags || "[]"),
1913
+ variables: JSON.parse(row.variables || "[]"),
1797
1914
  metadata: JSON.parse(row.metadata || "{}"),
1798
- priority: row.priority || "medium"
1915
+ priority: row.priority || "medium",
1916
+ version: row.version ?? 1
1799
1917
  };
1800
1918
  }
1801
1919
  function rowToTemplateTask(row) {
@@ -1804,7 +1922,9 @@ function rowToTemplateTask(row) {
1804
1922
  tags: JSON.parse(row.tags || "[]"),
1805
1923
  depends_on_positions: JSON.parse(row.depends_on_positions || "[]"),
1806
1924
  metadata: JSON.parse(row.metadata || "{}"),
1807
- priority: row.priority || "medium"
1925
+ priority: row.priority || "medium",
1926
+ condition: row.condition ?? null,
1927
+ include_template_id: row.include_template_id ?? null
1808
1928
  };
1809
1929
  }
1810
1930
  function resolveTemplateId(id, d) {
@@ -1813,14 +1933,15 @@ function resolveTemplateId(id, d) {
1813
1933
  function createTemplate(input, db) {
1814
1934
  const d = db || getDatabase();
1815
1935
  const id = uuid();
1816
- d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, project_id, plan_id, metadata, created_at)
1817
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1936
+ d.run(`INSERT INTO task_templates (id, name, title_pattern, description, priority, tags, variables, project_id, plan_id, metadata, created_at)
1937
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1818
1938
  id,
1819
1939
  input.name,
1820
1940
  input.title_pattern,
1821
1941
  input.description || null,
1822
1942
  input.priority || "medium",
1823
1943
  JSON.stringify(input.tags || []),
1944
+ JSON.stringify(input.variables || []),
1824
1945
  input.project_id || null,
1825
1946
  input.plan_id || null,
1826
1947
  JSON.stringify(input.metadata || {}),
@@ -1855,7 +1976,23 @@ function updateTemplate(id, updates, db) {
1855
1976
  const resolved = resolveTemplateId(id, d);
1856
1977
  if (!resolved)
1857
1978
  return null;
1858
- const sets = [];
1979
+ const current = getTemplateWithTasks(resolved, d);
1980
+ if (current) {
1981
+ const snapshot = JSON.stringify({
1982
+ name: current.name,
1983
+ title_pattern: current.title_pattern,
1984
+ description: current.description,
1985
+ priority: current.priority,
1986
+ tags: current.tags,
1987
+ variables: current.variables,
1988
+ project_id: current.project_id,
1989
+ plan_id: current.plan_id,
1990
+ metadata: current.metadata,
1991
+ tasks: current.tasks
1992
+ });
1993
+ d.run(`INSERT INTO template_versions (id, template_id, version, snapshot, created_at) VALUES (?, ?, ?, ?, ?)`, [uuid(), resolved, current.version, snapshot, now()]);
1994
+ }
1995
+ const sets = ["version = version + 1"];
1859
1996
  const values = [];
1860
1997
  if (updates.name !== undefined) {
1861
1998
  sets.push("name = ?");
@@ -1877,6 +2014,10 @@ function updateTemplate(id, updates, db) {
1877
2014
  sets.push("tags = ?");
1878
2015
  values.push(JSON.stringify(updates.tags));
1879
2016
  }
2017
+ if (updates.variables !== undefined) {
2018
+ sets.push("variables = ?");
2019
+ values.push(JSON.stringify(updates.variables));
2020
+ }
1880
2021
  if (updates.project_id !== undefined) {
1881
2022
  sets.push("project_id = ?");
1882
2023
  values.push(updates.project_id);
@@ -1889,8 +2030,6 @@ function updateTemplate(id, updates, db) {
1889
2030
  sets.push("metadata = ?");
1890
2031
  values.push(JSON.stringify(updates.metadata));
1891
2032
  }
1892
- if (sets.length === 0)
1893
- return getTemplate(resolved, d);
1894
2033
  values.push(resolved);
1895
2034
  d.run(`UPDATE task_templates SET ${sets.join(", ")} WHERE id = ?`, values);
1896
2035
  return getTemplate(resolved, d);
@@ -1920,8 +2059,8 @@ function addTemplateTasks(templateId, tasks, db) {
1920
2059
  for (let i = 0;i < tasks.length; i++) {
1921
2060
  const task = tasks[i];
1922
2061
  const id = uuid();
1923
- d.run(`INSERT INTO template_tasks (id, template_id, position, title_pattern, description, priority, tags, task_type, depends_on_positions, metadata, created_at)
1924
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
2062
+ d.run(`INSERT INTO template_tasks (id, template_id, position, title_pattern, description, priority, tags, task_type, condition, include_template_id, depends_on_positions, metadata, created_at)
2063
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
1925
2064
  id,
1926
2065
  templateId,
1927
2066
  i,
@@ -1930,6 +2069,8 @@ function addTemplateTasks(templateId, tasks, db) {
1930
2069
  task.priority || "medium",
1931
2070
  JSON.stringify(task.tags || []),
1932
2071
  task.task_type || null,
2072
+ task.condition || null,
2073
+ task.include_template_id || null,
1933
2074
  JSON.stringify(task.depends_on || []),
1934
2075
  JSON.stringify(task.metadata || {}),
1935
2076
  now()
@@ -1957,11 +2098,142 @@ function getTemplateTasks(templateId, db) {
1957
2098
  const rows = d.query("SELECT * FROM template_tasks WHERE template_id = ? ORDER BY position").all(resolved);
1958
2099
  return rows.map(rowToTemplateTask);
1959
2100
  }
1960
- function tasksFromTemplate(templateId, projectId, variables, taskListId, db) {
2101
+ function evaluateCondition(condition, variables) {
2102
+ if (!condition || condition.trim() === "")
2103
+ return true;
2104
+ const trimmed = condition.trim();
2105
+ const eqMatch = trimmed.match(/^\{([^}]+)\}\s*==\s*(.+)$/);
2106
+ if (eqMatch) {
2107
+ const varName = eqMatch[1];
2108
+ const expected = eqMatch[2].trim();
2109
+ return (variables[varName] ?? "") === expected;
2110
+ }
2111
+ const neqMatch = trimmed.match(/^\{([^}]+)\}\s*!=\s*(.+)$/);
2112
+ if (neqMatch) {
2113
+ const varName = neqMatch[1];
2114
+ const expected = neqMatch[2].trim();
2115
+ return (variables[varName] ?? "") !== expected;
2116
+ }
2117
+ const falsyMatch = trimmed.match(/^!\{([^}]+)\}$/);
2118
+ if (falsyMatch) {
2119
+ const varName = falsyMatch[1];
2120
+ const val = variables[varName];
2121
+ return !val || val === "" || val === "false";
2122
+ }
2123
+ const truthyMatch = trimmed.match(/^\{([^}]+)\}$/);
2124
+ if (truthyMatch) {
2125
+ const varName = truthyMatch[1];
2126
+ const val = variables[varName];
2127
+ return !!val && val !== "" && val !== "false";
2128
+ }
2129
+ return true;
2130
+ }
2131
+ function exportTemplate(id, db) {
2132
+ const d = db || getDatabase();
2133
+ const template = getTemplateWithTasks(id, d);
2134
+ if (!template)
2135
+ throw new Error(`Template not found: ${id}`);
2136
+ return {
2137
+ name: template.name,
2138
+ title_pattern: template.title_pattern,
2139
+ description: template.description,
2140
+ priority: template.priority,
2141
+ tags: template.tags,
2142
+ variables: template.variables,
2143
+ project_id: template.project_id,
2144
+ plan_id: template.plan_id,
2145
+ metadata: template.metadata,
2146
+ tasks: template.tasks.map((t) => ({
2147
+ position: t.position,
2148
+ title_pattern: t.title_pattern,
2149
+ description: t.description,
2150
+ priority: t.priority,
2151
+ tags: t.tags,
2152
+ task_type: t.task_type,
2153
+ condition: t.condition,
2154
+ include_template_id: t.include_template_id,
2155
+ depends_on_positions: t.depends_on_positions,
2156
+ metadata: t.metadata
2157
+ }))
2158
+ };
2159
+ }
2160
+ function importTemplate(json, db) {
2161
+ const d = db || getDatabase();
2162
+ const taskInputs = (json.tasks || []).map((t) => ({
2163
+ title_pattern: t.title_pattern,
2164
+ description: t.description ?? undefined,
2165
+ priority: t.priority,
2166
+ tags: t.tags,
2167
+ task_type: t.task_type ?? undefined,
2168
+ condition: t.condition ?? undefined,
2169
+ include_template_id: t.include_template_id ?? undefined,
2170
+ depends_on: t.depends_on_positions,
2171
+ metadata: t.metadata
2172
+ }));
2173
+ return createTemplate({
2174
+ name: json.name,
2175
+ title_pattern: json.title_pattern,
2176
+ description: json.description ?? undefined,
2177
+ priority: json.priority,
2178
+ tags: json.tags,
2179
+ variables: json.variables,
2180
+ project_id: json.project_id ?? undefined,
2181
+ plan_id: json.plan_id ?? undefined,
2182
+ metadata: json.metadata,
2183
+ tasks: taskInputs
2184
+ }, d);
2185
+ }
2186
+ function getTemplateVersion(id, version, db) {
2187
+ const d = db || getDatabase();
2188
+ const resolved = resolveTemplateId(id, d);
2189
+ if (!resolved)
2190
+ return null;
2191
+ const row = d.query("SELECT * FROM template_versions WHERE template_id = ? AND version = ?").get(resolved, version);
2192
+ return row || null;
2193
+ }
2194
+ function listTemplateVersions(id, db) {
2195
+ const d = db || getDatabase();
2196
+ const resolved = resolveTemplateId(id, d);
2197
+ if (!resolved)
2198
+ return [];
2199
+ return d.query("SELECT * FROM template_versions WHERE template_id = ? ORDER BY version DESC").all(resolved);
2200
+ }
2201
+ function resolveVariables(templateVars, provided) {
2202
+ const merged = { ...provided };
2203
+ for (const v of templateVars) {
2204
+ if (merged[v.name] === undefined && v.default !== undefined) {
2205
+ merged[v.name] = v.default;
2206
+ }
2207
+ }
2208
+ const missing = [];
2209
+ for (const v of templateVars) {
2210
+ if (v.required && merged[v.name] === undefined) {
2211
+ missing.push(v.name);
2212
+ }
2213
+ }
2214
+ if (missing.length > 0) {
2215
+ throw new Error(`Missing required template variable(s): ${missing.join(", ")}`);
2216
+ }
2217
+ return merged;
2218
+ }
2219
+ function substituteVars(text, variables) {
2220
+ let result = text;
2221
+ for (const [key, val] of Object.entries(variables)) {
2222
+ result = result.replace(new RegExp(`\\{${key}\\}`, "g"), val);
2223
+ }
2224
+ return result;
2225
+ }
2226
+ function tasksFromTemplate(templateId, projectId, variables, taskListId, db, _visitedTemplateIds) {
1961
2227
  const d = db || getDatabase();
1962
2228
  const template = getTemplateWithTasks(templateId, d);
1963
2229
  if (!template)
1964
2230
  throw new Error(`Template not found: ${templateId}`);
2231
+ const visited = _visitedTemplateIds || new Set;
2232
+ if (visited.has(template.id)) {
2233
+ throw new Error(`Circular template reference detected: ${template.id}`);
2234
+ }
2235
+ visited.add(template.id);
2236
+ const resolved = resolveVariables(template.variables, variables);
1965
2237
  if (template.tasks.length === 0) {
1966
2238
  const input = taskFromTemplate(templateId, { project_id: projectId, task_list_id: taskListId }, d);
1967
2239
  const task = createTask(input, d);
@@ -1969,16 +2241,27 @@ function tasksFromTemplate(templateId, projectId, variables, taskListId, db) {
1969
2241
  }
1970
2242
  const createdTasks = [];
1971
2243
  const positionToId = new Map;
2244
+ const skippedPositions = new Set;
1972
2245
  for (const tt of template.tasks) {
1973
- let title = tt.title_pattern;
1974
- let desc = tt.description;
1975
- if (variables) {
1976
- for (const [key, val] of Object.entries(variables)) {
1977
- title = title.replace(new RegExp(`\\{${key}\\}`, "g"), val);
1978
- if (desc)
1979
- desc = desc.replace(new RegExp(`\\{${key}\\}`, "g"), val);
2246
+ if (tt.include_template_id) {
2247
+ const includedTasks = tasksFromTemplate(tt.include_template_id, projectId, resolved, taskListId, d, visited);
2248
+ createdTasks.push(...includedTasks);
2249
+ if (includedTasks.length > 0) {
2250
+ positionToId.set(tt.position, includedTasks[0].id);
2251
+ } else {
2252
+ skippedPositions.add(tt.position);
1980
2253
  }
2254
+ continue;
2255
+ }
2256
+ if (tt.condition && !evaluateCondition(tt.condition, resolved)) {
2257
+ skippedPositions.add(tt.position);
2258
+ continue;
1981
2259
  }
2260
+ let title = tt.title_pattern;
2261
+ let desc = tt.description;
2262
+ title = substituteVars(title, resolved);
2263
+ if (desc)
2264
+ desc = substituteVars(desc, resolved);
1982
2265
  const task = createTask({
1983
2266
  title,
1984
2267
  description: desc ?? undefined,
@@ -1993,8 +2276,14 @@ function tasksFromTemplate(templateId, projectId, variables, taskListId, db) {
1993
2276
  positionToId.set(tt.position, task.id);
1994
2277
  }
1995
2278
  for (const tt of template.tasks) {
2279
+ if (skippedPositions.has(tt.position))
2280
+ continue;
2281
+ if (tt.include_template_id)
2282
+ continue;
1996
2283
  const deps = tt.depends_on_positions;
1997
2284
  for (const depPos of deps) {
2285
+ if (skippedPositions.has(depPos))
2286
+ continue;
1998
2287
  const taskId = positionToId.get(tt.position);
1999
2288
  const depId = positionToId.get(depPos);
2000
2289
  if (taskId && depId) {
@@ -2004,6 +2293,47 @@ function tasksFromTemplate(templateId, projectId, variables, taskListId, db) {
2004
2293
  }
2005
2294
  return createdTasks;
2006
2295
  }
2296
+ function previewTemplate(templateId, variables, db) {
2297
+ const d = db || getDatabase();
2298
+ const template = getTemplateWithTasks(templateId, d);
2299
+ if (!template)
2300
+ throw new Error(`Template not found: ${templateId}`);
2301
+ const resolved = resolveVariables(template.variables, variables);
2302
+ const tasks = [];
2303
+ if (template.tasks.length === 0) {
2304
+ tasks.push({
2305
+ position: 0,
2306
+ title: substituteVars(template.title_pattern, resolved),
2307
+ description: template.description ? substituteVars(template.description, resolved) : null,
2308
+ priority: template.priority,
2309
+ tags: template.tags,
2310
+ task_type: null,
2311
+ depends_on_positions: []
2312
+ });
2313
+ } else {
2314
+ for (const tt of template.tasks) {
2315
+ if (tt.condition && !evaluateCondition(tt.condition, resolved))
2316
+ continue;
2317
+ tasks.push({
2318
+ position: tt.position,
2319
+ title: substituteVars(tt.title_pattern, resolved),
2320
+ description: tt.description ? substituteVars(tt.description, resolved) : null,
2321
+ priority: tt.priority,
2322
+ tags: tt.tags,
2323
+ task_type: tt.task_type,
2324
+ depends_on_positions: tt.depends_on_positions
2325
+ });
2326
+ }
2327
+ }
2328
+ return {
2329
+ template_id: template.id,
2330
+ template_name: template.name,
2331
+ description: template.description,
2332
+ variables: template.variables,
2333
+ resolved_variables: resolved,
2334
+ tasks
2335
+ };
2336
+ }
2007
2337
 
2008
2338
  // src/db/checklists.ts
2009
2339
  function rowToItem(row) {
@@ -3798,6 +4128,107 @@ function clearActiveModel() {
3798
4128
  delete config.activeModel;
3799
4129
  writeConfig(config);
3800
4130
  }
4131
+ // src/db/builtin-templates.ts
4132
+ var BUILTIN_TEMPLATES = [
4133
+ {
4134
+ name: "open-source-project",
4135
+ description: "Full open-source project bootstrap \u2014 scaffold to publish",
4136
+ variables: [
4137
+ { name: "name", required: true, description: "Service name" },
4138
+ { name: "org", required: false, default: "hasna", description: "GitHub org" }
4139
+ ],
4140
+ tasks: [
4141
+ { position: 0, title_pattern: "Scaffold {name} package structure", priority: "critical" },
4142
+ { position: 1, title_pattern: "Create {name} SQLite database + migrations", priority: "critical", depends_on_positions: [0] },
4143
+ { position: 2, title_pattern: "Implement {name} CRUD operations", priority: "high", depends_on_positions: [1] },
4144
+ { position: 3, title_pattern: "Build {name} MCP server with standard tools", priority: "high", depends_on_positions: [2] },
4145
+ { position: 4, title_pattern: "Build {name} CLI with Commander.js", priority: "high", depends_on_positions: [2] },
4146
+ { position: 5, title_pattern: "Build {name} REST API", priority: "medium", depends_on_positions: [2] },
4147
+ { position: 6, title_pattern: "Write unit tests for {name}", priority: "high", depends_on_positions: [2, 3, 4] },
4148
+ { position: 7, title_pattern: "Add Apache 2.0 license and README", priority: "medium", depends_on_positions: [0] },
4149
+ { position: 8, title_pattern: "Create GitHub repo {org}/{name}", priority: "medium", depends_on_positions: [7] },
4150
+ { position: 9, title_pattern: "Add @hasna/cloud adapter", priority: "medium", depends_on_positions: [1] },
4151
+ { position: 10, title_pattern: "Write PostgreSQL migrations for {name}", priority: "medium", depends_on_positions: [1] },
4152
+ { position: 11, title_pattern: "Add feedback system + send_feedback MCP tool", priority: "medium", depends_on_positions: [3] },
4153
+ { position: 12, title_pattern: "Add agent tools (register_agent, heartbeat, set_focus, list_agents)", priority: "medium", depends_on_positions: [3] },
4154
+ { position: 13, title_pattern: "Create RDS database for {name}", priority: "low", depends_on_positions: [10] },
4155
+ { position: 14, title_pattern: "Publish @hasna/{name} to npm", priority: "high", depends_on_positions: [6, 7, 8] },
4156
+ { position: 15, title_pattern: "Install @hasna/{name} globally and verify", priority: "medium", depends_on_positions: [14] }
4157
+ ]
4158
+ },
4159
+ {
4160
+ name: "bug-fix",
4161
+ description: "Standard bug fix workflow",
4162
+ variables: [{ name: "bug", required: true, description: "Bug description" }],
4163
+ tasks: [
4164
+ { position: 0, title_pattern: "Reproduce: {bug}", priority: "critical" },
4165
+ { position: 1, title_pattern: "Diagnose root cause of {bug}", priority: "critical", depends_on_positions: [0] },
4166
+ { position: 2, title_pattern: "Implement fix for {bug}", priority: "critical", depends_on_positions: [1] },
4167
+ { position: 3, title_pattern: "Write regression test for {bug}", priority: "high", depends_on_positions: [2] },
4168
+ { position: 4, title_pattern: "Publish fix and verify in production", priority: "high", depends_on_positions: [3] }
4169
+ ]
4170
+ },
4171
+ {
4172
+ name: "feature",
4173
+ description: "Standard feature development workflow",
4174
+ variables: [{ name: "feature", required: true }, { name: "scope", required: false, default: "medium" }],
4175
+ tasks: [
4176
+ { position: 0, title_pattern: "Write spec for {feature}", priority: "high" },
4177
+ { position: 1, title_pattern: "Design implementation approach for {feature}", priority: "high", depends_on_positions: [0] },
4178
+ { position: 2, title_pattern: "Implement {feature}", priority: "critical", depends_on_positions: [1] },
4179
+ { position: 3, title_pattern: "Write tests for {feature}", priority: "high", depends_on_positions: [2] },
4180
+ { position: 4, title_pattern: "Code review for {feature}", priority: "medium", depends_on_positions: [3] },
4181
+ { position: 5, title_pattern: "Update docs for {feature}", priority: "medium", depends_on_positions: [2] },
4182
+ { position: 6, title_pattern: "Deploy {feature}", priority: "high", depends_on_positions: [4] }
4183
+ ]
4184
+ },
4185
+ {
4186
+ name: "security-audit",
4187
+ description: "Security audit workflow",
4188
+ variables: [{ name: "target", required: true }],
4189
+ tasks: [
4190
+ { position: 0, title_pattern: "Scan {target} for vulnerabilities", priority: "critical" },
4191
+ { position: 1, title_pattern: "Review {target} security findings", priority: "critical", depends_on_positions: [0] },
4192
+ { position: 2, title_pattern: "Fix critical issues in {target}", priority: "critical", depends_on_positions: [1] },
4193
+ { position: 3, title_pattern: "Retest {target} after fixes", priority: "high", depends_on_positions: [2] },
4194
+ { position: 4, title_pattern: "Write security report for {target}", priority: "medium", depends_on_positions: [3] },
4195
+ { position: 5, title_pattern: "Close audit for {target}", priority: "low", depends_on_positions: [4] }
4196
+ ]
4197
+ }
4198
+ ];
4199
+ function initBuiltinTemplates(db) {
4200
+ const d = db || getDatabase();
4201
+ const existing = listTemplates(d);
4202
+ const existingNames = new Set(existing.map((t) => t.name));
4203
+ let created = 0;
4204
+ let skipped = 0;
4205
+ const names = [];
4206
+ for (const bt of BUILTIN_TEMPLATES) {
4207
+ if (existingNames.has(bt.name)) {
4208
+ skipped++;
4209
+ continue;
4210
+ }
4211
+ const tasks = bt.tasks.map((t) => ({
4212
+ title_pattern: t.title_pattern,
4213
+ description: t.description,
4214
+ priority: t.priority,
4215
+ tags: t.tags,
4216
+ task_type: t.task_type,
4217
+ depends_on: t.depends_on_positions || t.depends_on,
4218
+ metadata: t.metadata
4219
+ }));
4220
+ createTemplate({
4221
+ name: bt.name,
4222
+ title_pattern: `${bt.name}: {${bt.variables[0]?.name || "name"}}`,
4223
+ description: bt.description,
4224
+ variables: bt.variables,
4225
+ tasks
4226
+ }, d);
4227
+ created++;
4228
+ names.push(bt.name);
4229
+ }
4230
+ return { created, skipped, names };
4231
+ }
3801
4232
  // src/db/handoffs.ts
3802
4233
  function createHandoff(input, db) {
3803
4234
  const d = db || getDatabase();
@@ -14751,6 +15182,26 @@ var PG_MIGRATIONS = [
14751
15182
  CREATE INDEX IF NOT EXISTS idx_template_tasks_template ON template_tasks(template_id);
14752
15183
 
14753
15184
  INSERT INTO _migrations (id) VALUES (37) ON CONFLICT DO NOTHING;
15185
+ `,
15186
+ `
15187
+ ALTER TABLE task_templates ADD COLUMN IF NOT EXISTS variables TEXT DEFAULT '[]';
15188
+ INSERT INTO _migrations (id) VALUES (38) ON CONFLICT DO NOTHING;
15189
+ `,
15190
+ `
15191
+ ALTER TABLE template_tasks ADD COLUMN IF NOT EXISTS condition TEXT;
15192
+ ALTER TABLE template_tasks ADD COLUMN IF NOT EXISTS include_template_id TEXT;
15193
+ ALTER TABLE task_templates ADD COLUMN IF NOT EXISTS version INTEGER NOT NULL DEFAULT 1;
15194
+
15195
+ CREATE TABLE IF NOT EXISTS template_versions (
15196
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
15197
+ template_id TEXT NOT NULL REFERENCES task_templates(id) ON DELETE CASCADE,
15198
+ version INTEGER NOT NULL,
15199
+ snapshot TEXT NOT NULL,
15200
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
15201
+ );
15202
+ CREATE INDEX IF NOT EXISTS idx_template_versions_template ON template_versions(template_id);
15203
+
15204
+ INSERT INTO _migrations (id) VALUES (39) ON CONFLICT DO NOTHING;
14754
15205
  `
14755
15206
  ];
14756
15207
 
@@ -15325,6 +15776,7 @@ export {
15325
15776
  searchTasks,
15326
15777
  scoreTask,
15327
15778
  saveSnapshot,
15779
+ resolveVariables,
15328
15780
  resolvePartialId,
15329
15781
  resetDatabase,
15330
15782
  removeTaskRelationshipByPair,
@@ -15338,6 +15790,7 @@ export {
15338
15790
  releaseAgent,
15339
15791
  registerAgent,
15340
15792
  redistributeStaleTasks,
15793
+ previewTemplate,
15341
15794
  patrolTasks,
15342
15795
  parseRecurrenceRule,
15343
15796
  parseGitHubUrl,
@@ -15354,6 +15807,7 @@ export {
15354
15807
  loadConfig,
15355
15808
  listWebhooks,
15356
15809
  listTemplates,
15810
+ listTemplateVersions,
15357
15811
  listTasks,
15358
15812
  listTaskLists,
15359
15813
  listTaskFiles,
@@ -15364,14 +15818,18 @@ export {
15364
15818
  listPlans,
15365
15819
  listOrgs,
15366
15820
  listHandoffs,
15821
+ listDeliveries,
15367
15822
  listComments,
15368
15823
  listAgents,
15369
15824
  issueToTask,
15370
15825
  isValidRecurrenceRule,
15371
15826
  isAgentConflict,
15827
+ initBuiltinTemplates,
15828
+ importTemplate,
15372
15829
  getWebhook,
15373
15830
  getTraceStats,
15374
15831
  getTemplateWithTasks,
15832
+ getTemplateVersion,
15375
15833
  getTemplateTasks,
15376
15834
  getTemplate,
15377
15835
  getTasksChangedSince,
@@ -15432,6 +15890,8 @@ export {
15432
15890
  failTask,
15433
15891
  extractTodos,
15434
15892
  extractFromSource,
15893
+ exportTemplate,
15894
+ evaluateCondition,
15435
15895
  ensureTaskList,
15436
15896
  ensureProject,
15437
15897
  dispatchWebhook,
@@ -15501,5 +15961,6 @@ export {
15501
15961
  DependencyCycleError,
15502
15962
  DEFAULT_MODEL,
15503
15963
  CompletionGuardError,
15964
+ BUILTIN_TEMPLATES,
15504
15965
  AgentNotFoundError
15505
15966
  };