syntaur 0.64.0 → 0.66.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dashboard/dist/assets/{_basePickBy-CwwFEBj8.js → _basePickBy-CrrGp7iQ.js} +1 -1
  3. package/dashboard/dist/assets/{_baseUniq-BoxAvlar.js → _baseUniq-CR-L9Z4l.js} +1 -1
  4. package/dashboard/dist/assets/{arc-DohD31yM.js → arc-Bplhanol.js} +1 -1
  5. package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-J4SdvLJs.js → architectureDiagram-2XIMDMQ5-hcuAyqPz.js} +1 -1
  6. package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-BHuFRK4q.js → blockDiagram-WCTKOSBZ-BkGOQX1p.js} +1 -1
  7. package/dashboard/dist/assets/{c4Diagram-IC4MRINW-C5jiXpiB.js → c4Diagram-IC4MRINW-CyhVhe9Z.js} +1 -1
  8. package/dashboard/dist/assets/channel-DKDXpYJX.js +1 -0
  9. package/dashboard/dist/assets/{chunk-4BX2VUAB-CcShTIhb.js → chunk-4BX2VUAB-DWT4Kb7h.js} +1 -1
  10. package/dashboard/dist/assets/{chunk-55IACEB6-BJDZp3Ih.js → chunk-55IACEB6-DabzdGiV.js} +1 -1
  11. package/dashboard/dist/assets/{chunk-FMBD7UC4-DgX8V3qh.js → chunk-FMBD7UC4-9u9EbTn2.js} +1 -1
  12. package/dashboard/dist/assets/{chunk-JSJVCQXG-DnYWrOFd.js → chunk-JSJVCQXG-qNUjUgoq.js} +1 -1
  13. package/dashboard/dist/assets/{chunk-KX2RTZJC-CvsjbfkS.js → chunk-KX2RTZJC-Iw8btz_i.js} +1 -1
  14. package/dashboard/dist/assets/{chunk-NQ4KR5QH-DVLwGTNn.js → chunk-NQ4KR5QH-DuWW63Ax.js} +1 -1
  15. package/dashboard/dist/assets/{chunk-QZHKN3VN-CFNsM5VV.js → chunk-QZHKN3VN-CquL_I3V.js} +1 -1
  16. package/dashboard/dist/assets/{chunk-WL4C6EOR-BgvnUwmv.js → chunk-WL4C6EOR-CmAQtVO_.js} +1 -1
  17. package/dashboard/dist/assets/classDiagram-VBA2DB6C-U7OD7LGq.js +1 -0
  18. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-U7OD7LGq.js +1 -0
  19. package/dashboard/dist/assets/clone-CZUxWIoY.js +1 -0
  20. package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-C4aLmHe0.js → cose-bilkent-S5V4N54A-D_vWs01G.js} +1 -1
  21. package/dashboard/dist/assets/{dagre-KLK3FWXG-BN-9NtEU.js → dagre-KLK3FWXG-DRSd99h1.js} +1 -1
  22. package/dashboard/dist/assets/{diagram-E7M64L7V-CeYAeYg6.js → diagram-E7M64L7V-CYIlPZb0.js} +1 -1
  23. package/dashboard/dist/assets/{diagram-IFDJBPK2-DwKlGh9_.js → diagram-IFDJBPK2-D1Mk78S1.js} +1 -1
  24. package/dashboard/dist/assets/{diagram-P4PSJMXO-Brl0MDaZ.js → diagram-P4PSJMXO-cK1mot2F.js} +1 -1
  25. package/dashboard/dist/assets/{erDiagram-INFDFZHY-DUyLXkUI.js → erDiagram-INFDFZHY-CKAce3P6.js} +1 -1
  26. package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-DBgZTa3b.js → flowDiagram-PKNHOUZH-B8UyrAbG.js} +1 -1
  27. package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-CTMXhGer.js → ganttDiagram-A5KZAMGK-DFlWro9k.js} +1 -1
  28. package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-CMeLom6N.js → gitGraphDiagram-K3NZZRJ6-CfvsGSZk.js} +1 -1
  29. package/dashboard/dist/assets/{graph-BQZrNogB.js → graph-DTnszQr1.js} +1 -1
  30. package/dashboard/dist/assets/{index-tUNHiaW4.js → index-BukhcQ51.js} +127 -127
  31. package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-CfiYCGUc.js → infoDiagram-LFFYTUFH-B4CQ1F6_.js} +1 -1
  32. package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-WSdBFOoI.js → ishikawaDiagram-PHBUUO56-BJf9J-fu.js} +1 -1
  33. package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-u-S5-YRq.js → journeyDiagram-4ABVD52K-CDt3Ieap.js} +1 -1
  34. package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-B5sU56Sm.js → kanban-definition-K7BYSVSG-DvenMiPw.js} +1 -1
  35. package/dashboard/dist/assets/{layout-CH9z9Vzx.js → layout-B_3cS6Wx.js} +1 -1
  36. package/dashboard/dist/assets/{linear-C-SiDLxL.js → linear-VVB-1jRr.js} +1 -1
  37. package/dashboard/dist/assets/{mermaid.core-CGgij72_.js → mermaid.core-BR4E6mM3.js} +4 -4
  38. package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-yu7UCSjb.js → mindmap-definition-YRQLILUH-BSMVANTC.js} +1 -1
  39. package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-BGSUgFeI.js → pieDiagram-SKSYHLDU-Bc1KLLbU.js} +1 -1
  40. package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-XnJqntVQ.js → quadrantDiagram-337W2JSQ-DCxPDJLj.js} +1 -1
  41. package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-CZ5tl3qr.js → requirementDiagram-Z7DCOOCP-CIR7dfaq.js} +1 -1
  42. package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-CgcGiKDB.js → sankeyDiagram-WA2Y5GQK-BSmLrby0.js} +1 -1
  43. package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE--GDQSqiF.js → sequenceDiagram-2WXFIKYE-D8SKlaHf.js} +1 -1
  44. package/dashboard/dist/assets/{stateDiagram-RAJIS63D-sbaNsQ3O.js → stateDiagram-RAJIS63D-P-weHmzQ.js} +1 -1
  45. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-DgvK3I0Y.js +1 -0
  46. package/dashboard/dist/assets/{timeline-definition-YZTLITO2-DieNq8qp.js → timeline-definition-YZTLITO2-AWMvjwea.js} +1 -1
  47. package/dashboard/dist/assets/{treemap-KZPCXAKY-CND0AGzG.js → treemap-KZPCXAKY-ByTswXsP.js} +1 -1
  48. package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-C32CbTtv.js → vennDiagram-LZ73GAT5-FhcL4LCg.js} +1 -1
  49. package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-I-8Tu4ki.js → xychartDiagram-JWTSCODW-Aql6py8q.js} +1 -1
  50. package/dashboard/dist/index.html +1 -1
  51. package/dist/dashboard/server.js +90 -20
  52. package/dist/dashboard/server.js.map +1 -1
  53. package/dist/db/leases-db.d.ts +4 -3
  54. package/dist/db/leases-db.js +6 -4
  55. package/dist/db/leases-db.js.map +1 -1
  56. package/dist/index.js +148 -27
  57. package/dist/index.js.map +1 -1
  58. package/dist/launch/index.js +20 -7
  59. package/dist/launch/index.js.map +1 -1
  60. package/package.json +1 -1
  61. package/platforms/claude-code/.claude-plugin/plugin.json +1 -1
  62. package/platforms/codex/.codex-plugin/plugin.json +1 -1
  63. package/platforms/hermes/plugins/syntaur/__pycache__/__init__.cpython-312.pyc +0 -0
  64. package/platforms/hermes/plugins/syntaur/__pycache__/boundary.cpython-312.pyc +0 -0
  65. package/dashboard/dist/assets/channel-3-4_gf6X.js +0 -1
  66. package/dashboard/dist/assets/classDiagram-VBA2DB6C-CrATshpz.js +0 -1
  67. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-CrATshpz.js +0 -1
  68. package/dashboard/dist/assets/clone-CfV5MG52.js +0 -1
  69. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-Cr-Lm_NG.js +0 -1
package/dist/index.js CHANGED
@@ -516,7 +516,9 @@ function parseDecisionRecord(fileContent) {
516
516
  function parseComments(fileContent) {
517
517
  const [fm, body] = extractFrontmatter(fileContent);
518
518
  const entries = [];
519
- const sections = body.split(/^## /m).slice(1);
519
+ const sections = body.split(
520
+ /^## (?=[^\n]*\n\s*\*\*Recorded:\*\*[^\n]*\n\*\*Author:\*\*[^\n]*\n\*\*Type:\*\*\s*(?:question|note|feedback)\b)/m
521
+ ).slice(1);
520
522
  for (const section of sections) {
521
523
  const newlineIdx = section.indexOf("\n");
522
524
  if (newlineIdx === -1) continue;
@@ -1198,7 +1200,7 @@ function formatYamlValue(value) {
1198
1200
  if (/^(null|~|true|false|-?\d+(\.\d+)?)$/i.test(value)) {
1199
1201
  return `"${value}"`;
1200
1202
  }
1201
- if (/[:#{}[\],&*?|>!%@\`]/.test(value) || /^\s|\s$/.test(value) || value === "") {
1203
+ if (/[:#{}[\],&*?|>!%@\`]/.test(value) || /^\s|\s$/.test(value) || /^["']|["']$/.test(value) || value === "") {
1202
1204
  const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1203
1205
  return `"${escaped}"`;
1204
1206
  }
@@ -1225,7 +1227,7 @@ ${key}: ${formatted}${result.slice(closeIdx)}`;
1225
1227
  function findWorkspaceBlock(fmBlock) {
1226
1228
  const headerMatch = fmBlock.match(/^workspace:\s*$/m);
1227
1229
  if (!headerMatch) return null;
1228
- const headerStart = fmBlock.indexOf(headerMatch[0]);
1230
+ const headerStart = headerMatch.index ?? fmBlock.indexOf(headerMatch[0]);
1229
1231
  const bodyStart = headerStart + headerMatch[0].length + 1;
1230
1232
  const after = fmBlock.slice(bodyStart);
1231
1233
  const lines = after.split("\n");
@@ -1283,7 +1285,7 @@ function renameStatusInHistory(content, oldId, newId) {
1283
1285
  if (!fmMatch) return content;
1284
1286
  const esc = oldId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1285
1287
  const re = new RegExp(`^(\\s+(?:from|to|phaseFrom|phaseTo):[ \\t]*)("?)${esc}\\2[ \\t]*$`, "gm");
1286
- const newFm = fmMatch[2].replace(re, (_m, prefix, quote) => `${prefix}${quote}${newId}${quote}`);
1288
+ const newFm = fmMatch[2].replace(re, (_m, prefix) => `${prefix}${formatYamlValue(newId)}`);
1287
1289
  return `${fmMatch[1]}${newFm}${fmMatch[3]}${content.slice(fmMatch[0].length)}`;
1288
1290
  }
1289
1291
  function findStatusHistoryBlock(fmBlock) {
@@ -1639,12 +1641,14 @@ var parser_exports = {};
1639
1641
  __export(parser_exports, {
1640
1642
  appendLogEntry: () => appendLogEntry,
1641
1643
  archivePath: () => archivePath,
1644
+ assertValidTags: () => assertValidTags,
1642
1645
  checklistPath: () => checklistPath,
1643
1646
  computeCounts: () => computeCounts,
1644
1647
  decodeMetaValue: () => decodeMetaValue,
1645
1648
  encodeMetaValue: () => encodeMetaValue,
1646
1649
  generateShortId: () => generateShortId,
1647
1650
  generateUniqueId: () => generateUniqueId,
1651
+ isValidTag: () => isValidTag,
1648
1652
  logPath: () => logPath,
1649
1653
  parseChecklist: () => parseChecklist,
1650
1654
  parseChecklistItem: () => parseChecklistItem,
@@ -1674,6 +1678,18 @@ function generateUniqueId(existingIds) {
1674
1678
  }
1675
1679
  return id;
1676
1680
  }
1681
+ function isValidTag(tag) {
1682
+ return typeof tag === "string" && VALID_TAG_REGEX.test(tag);
1683
+ }
1684
+ function assertValidTags(tags) {
1685
+ for (const t of tags) {
1686
+ if (!isValidTag(t)) {
1687
+ throw new Error(
1688
+ `Invalid tag ${JSON.stringify(t)}: tags may contain only letters, digits, '-' and '_' (no spaces, newlines, or '#').`
1689
+ );
1690
+ }
1691
+ }
1692
+ }
1677
1693
  function encodeMetaValue(value) {
1678
1694
  let out = "";
1679
1695
  for (const ch of value) {
@@ -1865,6 +1881,7 @@ function parseChecklistItem(line) {
1865
1881
  }
1866
1882
  function serializeChecklistItem(item) {
1867
1883
  const marker = statusToMarker(item);
1884
+ assertValidTags(item.tags);
1868
1885
  const tagStr = item.tags.map((t) => `#${t}`).join(" ");
1869
1886
  const parts = [`- [${marker}] ${escapeDescription(item.description)}`];
1870
1887
  if (tagStr) parts.push(tagStr);
@@ -2054,7 +2071,7 @@ function computeCounts(items) {
2054
2071
  }
2055
2072
  return counts;
2056
2073
  }
2057
- var ITEM_REGEX, ID_REGEX, TAG_REGEX, META_TOKEN_REGEX, META_ENCODE_CHARS;
2074
+ var ITEM_REGEX, ID_REGEX, TAG_REGEX, META_TOKEN_REGEX, META_ENCODE_CHARS, VALID_TAG_REGEX;
2058
2075
  var init_parser2 = __esm({
2059
2076
  "src/todos/parser.ts"() {
2060
2077
  "use strict";
@@ -2065,6 +2082,7 @@ var init_parser2 = __esm({
2065
2082
  TAG_REGEX = /#([a-zA-Z0-9_-]+)/g;
2066
2083
  META_TOKEN_REGEX = /\[t:[a-f0-9]{4}\]\s+<([^>]*)>\s*$/;
2067
2084
  META_ENCODE_CHARS = ["%", "<", ">", "[", "]", "=", ";", "\n", "\r"];
2085
+ VALID_TAG_REGEX = /^[a-zA-Z0-9_-]+$/;
2068
2086
  }
2069
2087
  });
2070
2088
 
@@ -4072,6 +4090,16 @@ function parseStatusConfig(content) {
4072
4090
  }
4073
4091
  return t;
4074
4092
  };
4093
+ const unquoteAql = (v) => {
4094
+ const t = v.trim();
4095
+ if (t.startsWith('"') && t.endsWith('"') && t.length >= 2) {
4096
+ return t.slice(1, -1).replace(/\\(["\\])/g, "$1");
4097
+ }
4098
+ if (t.startsWith("'") && t.endsWith("'") && t.length >= 2) {
4099
+ return t.slice(1, -1);
4100
+ }
4101
+ return t;
4102
+ };
4075
4103
  let currentSection = null;
4076
4104
  const lines = remaining.split("\n");
4077
4105
  function parseListEntry(lineIdx, baseIndent) {
@@ -4151,8 +4179,8 @@ function parseStatusConfig(content) {
4151
4179
  if (entry["phase"] && entry["when"] !== void 0) {
4152
4180
  phaseLadder.push({
4153
4181
  phase: unquote(entry["phase"]),
4154
- when: unquote(entry["when"]),
4155
- next: entry["next"] !== void 0 ? unquote(entry["next"]) : void 0
4182
+ when: unquoteAql(entry["when"]),
4183
+ next: entry["next"] !== void 0 ? unquoteAql(entry["next"]) : void 0
4156
4184
  });
4157
4185
  }
4158
4186
  lineIdx += consumed - 1;
@@ -4163,7 +4191,7 @@ function parseStatusConfig(content) {
4163
4191
  if (entry["else"] !== void 0) {
4164
4192
  disposition.push({ when: null, is: unquote(entry["else"]) });
4165
4193
  } else if (entry["when"] !== void 0 && entry["is"]) {
4166
- disposition.push({ when: unquote(entry["when"]), is: unquote(entry["is"]) });
4194
+ disposition.push({ when: unquoteAql(entry["when"]), is: unquote(entry["is"]) });
4167
4195
  }
4168
4196
  lineIdx += consumed - 1;
4169
4197
  continue;
@@ -4227,6 +4255,7 @@ function buildDefaultStatusConfig() {
4227
4255
  }
4228
4256
  function serializeStatusConfig(statuses) {
4229
4257
  const lines = [];
4258
+ const escapeAql = (s) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
4230
4259
  lines.push("statuses:");
4231
4260
  lines.push(" definitions:");
4232
4261
  for (const s of statuses.statuses) {
@@ -4267,15 +4296,15 @@ function serializeStatusConfig(statuses) {
4267
4296
  lines.push(" phaseLadder:");
4268
4297
  for (const rung of d.phaseLadder) {
4269
4298
  lines.push(` - phase: ${rung.phase}`);
4270
- lines.push(` when: "${rung.when.replace(/"/g, '\\"')}"`);
4271
- if (rung.next) lines.push(` next: "${rung.next.replace(/"/g, '\\"')}"`);
4299
+ lines.push(` when: "${escapeAql(rung.when)}"`);
4300
+ if (rung.next !== void 0) lines.push(` next: "${escapeAql(rung.next)}"`);
4272
4301
  }
4273
4302
  lines.push(" disposition:");
4274
4303
  for (const rule of d.disposition) {
4275
4304
  if (rule.when === null) {
4276
4305
  lines.push(` - else: ${rule.is}`);
4277
4306
  } else {
4278
- lines.push(` - when: "${rule.when.replace(/"/g, '\\"')}"`);
4307
+ lines.push(` - when: "${escapeAql(rule.when)}"`);
4279
4308
  lines.push(` is: ${rule.is}`);
4280
4309
  }
4281
4310
  }
@@ -17365,6 +17394,14 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
17365
17394
  res.status(400).json({ error: "body is required" });
17366
17395
  return;
17367
17396
  }
17397
+ if (typeof author === "string" && /[\r\n]/.test(author)) {
17398
+ res.status(400).json({ error: "author must not contain newlines" });
17399
+ return;
17400
+ }
17401
+ if (typeof replyTo === "string" && /[\r\n]/.test(replyTo)) {
17402
+ res.status(400).json({ error: "replyTo must not contain newlines" });
17403
+ return;
17404
+ }
17368
17405
  const commentType = type && ["question", "note", "feedback"].includes(type) ? type : "note";
17369
17406
  const timestamp = nowTimestamp();
17370
17407
  const entryAuthor = typeof author === "string" && author.trim() ? author.trim() : "human";
@@ -18974,6 +19011,14 @@ async function appendCommentTo(assignmentDir, assignmentRef, req2, res, reloadDe
18974
19011
  }
18975
19012
  const commentType = type && ["question", "note", "feedback"].includes(type) ? type : "note";
18976
19013
  const timestamp = nowTimestamp();
19014
+ if (typeof author === "string" && /[\r\n]/.test(author)) {
19015
+ res.status(400).json({ error: "author must not contain newlines" });
19016
+ return;
19017
+ }
19018
+ if (typeof replyTo === "string" && /[\r\n]/.test(replyTo)) {
19019
+ res.status(400).json({ error: "replyTo must not contain newlines" });
19020
+ return;
19021
+ }
18977
19022
  const entryAuthor = typeof author === "string" && author.trim() ? author.trim() : "human";
18978
19023
  let currentContent;
18979
19024
  let currentCount = 0;
@@ -22111,10 +22156,12 @@ function getLeaseEvents(lease_id, limit = 50) {
22111
22156
  const database = getLeasesDb();
22112
22157
  if (lease_id) {
22113
22158
  return database.prepare(
22114
- `SELECT id, lease_id, event, at, detail_json
22115
- FROM lease_events WHERE lease_id = ?
22116
- ORDER BY at ASC, id ASC
22117
- LIMIT ?`
22159
+ `SELECT id, lease_id, event, at, detail_json FROM (
22160
+ SELECT id, lease_id, event, at, detail_json
22161
+ FROM lease_events WHERE lease_id = ?
22162
+ ORDER BY at DESC, id DESC
22163
+ LIMIT ?
22164
+ ) ORDER BY at ASC, id ASC`
22118
22165
  ).all(lease_id, limit);
22119
22166
  }
22120
22167
  return database.prepare(
@@ -22619,19 +22666,18 @@ function evaluateKind(trigger, job, ctx) {
22619
22666
  }
22620
22667
  }
22621
22668
  function evaluateCron(trigger, now, createdAtMs) {
22622
- let cron;
22623
22669
  try {
22624
- cron = new Cron(trigger.expr, trigger.tz ? { timezone: trigger.tz } : {});
22670
+ const cron = new Cron(trigger.expr, trigger.tz ? { timezone: trigger.tz } : {});
22671
+ const prevs = cron.previousRuns(1, new Date(now.getTime() + 1e3));
22672
+ let prev = prevs.length > 0 ? prevs[0] : null;
22673
+ if (prev && prev.getTime() > now.getTime()) prev = null;
22674
+ if (prev && !Number.isNaN(createdAtMs) && prev.getTime() < createdAtMs) prev = null;
22675
+ const next = cron.nextRun(now);
22676
+ if (!prev) return { due: false, nextFireIso: next ? iso(next) : null };
22677
+ return { due: true, dedupeKey: `cron:${iso(prev)}`, nextFireIso: next ? iso(next) : null };
22625
22678
  } catch {
22626
22679
  return notDue;
22627
22680
  }
22628
- const prevs = cron.previousRuns(1, new Date(now.getTime() + 1e3));
22629
- let prev = prevs.length > 0 ? prevs[0] : null;
22630
- if (prev && prev.getTime() > now.getTime()) prev = null;
22631
- if (prev && !Number.isNaN(createdAtMs) && prev.getTime() < createdAtMs) prev = null;
22632
- const next = cron.nextRun(now);
22633
- if (!prev) return { due: false, nextFireIso: next ? iso(next) : null };
22634
- return { due: true, dedupeKey: `cron:${iso(prev)}`, nextFireIso: next ? iso(next) : null };
22635
22681
  }
22636
22682
  function evaluateAfterReset(trigger, now) {
22637
22683
  const v = verifyReset(trigger.provider, trigger.anchor, now);
@@ -24951,6 +24997,10 @@ function createTodosRouter(todosDir2, broadcast, projectsDir2) {
24951
24997
  res.status(400).json({ error: "description is required" });
24952
24998
  return;
24953
24999
  }
25000
+ if (tags !== void 0 && (!Array.isArray(tags) || !tags.every(isValidTag))) {
25001
+ res.status(400).json({ error: "tags must be an array of [a-zA-Z0-9_-] strings (no spaces, newlines, or '#')" });
25002
+ return;
25003
+ }
24954
25004
  const item = await wsLock(workspace, async () => {
24955
25005
  const checklist = await readChecklist(todosDir2, workspace);
24956
25006
  const existingIds = new Set(checklist.items.map((i) => i.id));
@@ -25057,6 +25107,7 @@ workspace: ${workspace}
25057
25107
  }
25058
25108
  const completedItems = checklist.items.filter((i) => completedIds.has(i.id));
25059
25109
  for (const item of completedItems) {
25110
+ assertValidTags(item.tags);
25060
25111
  archContent += `- [x] ${item.description} ${item.tags.map((t) => `#${t}`).join(" ")} [t:${item.id}]
25061
25112
  `;
25062
25113
  }
@@ -25122,6 +25173,14 @@ workspace: ${workspace}
25122
25173
  router.patch("/:workspace/:id", async (req2, res) => {
25123
25174
  try {
25124
25175
  const workspace = getWorkspaceParam(req2.params.workspace);
25176
+ if (req2.body.tags !== void 0 && (!Array.isArray(req2.body.tags) || !req2.body.tags.every(isValidTag))) {
25177
+ res.status(400).json({ error: "tags must be an array of [a-zA-Z0-9_-] strings (no spaces, newlines, or '#')" });
25178
+ return;
25179
+ }
25180
+ if (req2.body.description !== void 0 && typeof req2.body.description !== "string") {
25181
+ res.status(400).json({ error: "description must be a string" });
25182
+ return;
25183
+ }
25125
25184
  const result = await wsLock(workspace, async () => {
25126
25185
  const checklist = await readChecklist(todosDir2, workspace);
25127
25186
  const item = checklist.items.find((i) => i.id === req2.params.id);
@@ -25660,6 +25719,10 @@ function createProjectTodosRouter(projectsDir2, broadcast, workspaceTodosDir) {
25660
25719
  res.status(400).json({ error: "description is required" });
25661
25720
  return;
25662
25721
  }
25722
+ if (tags !== void 0 && (!Array.isArray(tags) || !tags.every(isValidTag))) {
25723
+ res.status(400).json({ error: "tags must be an array of [a-zA-Z0-9_-] strings (no spaces, newlines, or '#')" });
25724
+ return;
25725
+ }
25663
25726
  if (!await projectExists(projectsDir2, slug)) {
25664
25727
  notFound(res, slug);
25665
25728
  return;
@@ -25804,6 +25867,7 @@ workspace: ${slug}
25804
25867
  }
25805
25868
  const completedItems = checklist.items.filter((i) => completedIds.has(i.id));
25806
25869
  for (const item of completedItems) {
25870
+ assertValidTags(item.tags);
25807
25871
  archContent += `- [x] ${item.description} ${item.tags.map((t) => `#${t}`).join(" ")} [t:${item.id}]
25808
25872
  `;
25809
25873
  }
@@ -25889,6 +25953,14 @@ workspace: ${slug}
25889
25953
  router.patch("/:id", async (req2, res) => {
25890
25954
  try {
25891
25955
  const slug = getProjectIdParam(params(req2).projectId);
25956
+ if (req2.body.tags !== void 0 && (!Array.isArray(req2.body.tags) || !req2.body.tags.every(isValidTag))) {
25957
+ res.status(400).json({ error: "tags must be an array of [a-zA-Z0-9_-] strings (no spaces, newlines, or '#')" });
25958
+ return;
25959
+ }
25960
+ if (req2.body.description !== void 0 && typeof req2.body.description !== "string") {
25961
+ res.status(400).json({ error: "description must be a string" });
25962
+ return;
25963
+ }
25892
25964
  if (!await projectExists(projectsDir2, slug)) {
25893
25965
  notFound(res, slug);
25894
25966
  return;
@@ -34014,6 +34086,11 @@ todoCommand.command("add").description("Add a new todo item").argument("<descrip
34014
34086
  const existingIds = new Set(checklist.items.map((i) => i.id));
34015
34087
  const id = generateUniqueId(existingIds);
34016
34088
  const tags = options.tags ? options.tags.split(",").map((t) => t.trim()) : [];
34089
+ const badTag = tags.find((t) => !isValidTag(t));
34090
+ if (badTag !== void 0) {
34091
+ console.error(`Invalid tag ${JSON.stringify(badTag)}: tags may contain only letters, digits, '-' and '_' (no spaces, newlines, or '#').`);
34092
+ process.exit(1);
34093
+ }
34017
34094
  const now = nowISO2();
34018
34095
  const item = {
34019
34096
  id,
@@ -34247,6 +34324,11 @@ todoCommand.command("tag").description("Modify tags on a todo").argument("<id>",
34247
34324
  }
34248
34325
  if (options.add) {
34249
34326
  const toAdd = options.add.split(",").map((t) => t.trim());
34327
+ const badTag = toAdd.find((t) => !isValidTag(t));
34328
+ if (badTag !== void 0) {
34329
+ console.error(`Invalid tag ${JSON.stringify(badTag)}: tags may contain only letters, digits, '-' and '_' (no spaces, newlines, or '#').`);
34330
+ process.exit(1);
34331
+ }
34250
34332
  for (const tag of toAdd) {
34251
34333
  if (!item.tags.includes(tag)) item.tags.push(tag);
34252
34334
  }
@@ -34325,6 +34407,7 @@ workspace: ${workspace}
34325
34407
  }
34326
34408
  const completedItems = checklist.items.filter((i) => completedIds.has(i.id));
34327
34409
  for (const item of completedItems) {
34410
+ assertValidTags(item.tags);
34328
34411
  archContent += `- [x] ${item.description} ${item.tags.map((t) => `#${t}`).join(" ")} [t:${item.id}]
34329
34412
  `;
34330
34413
  }
@@ -34617,6 +34700,11 @@ async function moveTodo(id, options) {
34617
34700
  throw new Error(`Todo [t:${id}] not found in scope ${describeScope(sourceScope)}.`);
34618
34701
  }
34619
34702
  const item = sourceChecklist.items[idx];
34703
+ if (item.bundleId !== null) {
34704
+ throw new Error(
34705
+ `Todo [t:${id}] is part of bundle b:${item.bundleId}; run \`syntaur todo bundle remove b:${item.bundleId} ${id}\` first.`
34706
+ );
34707
+ }
34620
34708
  if (targetChecklist.items.some((i) => i.id === id)) {
34621
34709
  throw new Error(`Todo id [t:${id}] already exists in target scope ${describeScope(targetScope)}; refusing to move (collision).`);
34622
34710
  }
@@ -39752,7 +39840,14 @@ function parseDuration(input4, fallbackSeconds) {
39752
39840
  const n = Number.parseInt(match[1], 10);
39753
39841
  const unit = (match[2] ?? "s").toLowerCase();
39754
39842
  const mult = { s: 1, m: 60, h: 3600, d: 86400 };
39755
- return n * mult[unit];
39843
+ const seconds = n * mult[unit];
39844
+ const MAX_DURATION_SECONDS = 100 * 365 * 86400;
39845
+ if (seconds > MAX_DURATION_SECONDS) {
39846
+ throw new Error(
39847
+ `invalid duration "${input4}" \u2014 too large (max ${MAX_DURATION_SECONDS}s \u2248 100 years)`
39848
+ );
39849
+ }
39850
+ return seconds;
39756
39851
  }
39757
39852
  function parseMetadataFlags(values) {
39758
39853
  if (!values || values.length === 0) return void 0;
@@ -39842,6 +39937,11 @@ leaseCommand.command("claim").description("Claim an idle member of an inventory.
39842
39937
  return;
39843
39938
  }
39844
39939
  const ttl_s = parseDuration(opts.ttl, detail.inventory.default_ttl_s);
39940
+ if (ttl_s <= 0) {
39941
+ console.error("Error: --ttl must be positive");
39942
+ process.exit(1);
39943
+ return;
39944
+ }
39845
39945
  const waitBudgetMs = opts.wait !== void 0 ? parseDuration(opts.wait, 0) * 1e3 : 0;
39846
39946
  const tryClaim = () => claimLease(inventory, ttl_s, opts.for);
39847
39947
  let result;
@@ -40213,6 +40313,7 @@ memberCommand.command("list").description("List all members of an inventory (pur
40213
40313
  // src/commands/schedule.ts
40214
40314
  init_config2();
40215
40315
  import { Command as Command8 } from "commander";
40316
+ import { Cron as Cron2 } from "croner";
40216
40317
  init_agent_sessions();
40217
40318
  init_session_db();
40218
40319
  init_terminal_schema();
@@ -40388,7 +40489,21 @@ function buildTrigger(opts) {
40388
40489
  const chosen = [];
40389
40490
  if (opts.at) chosen.push({ kind: "at", at: opts.at });
40390
40491
  if (opts.in) chosen.push({ kind: "in", durationMs: parseDurationMs(opts.in), anchorIso: nowTimestamp() });
40391
- if (opts.cron) chosen.push({ kind: "cron", expr: opts.cron, ...opts.tz ? { tz: opts.tz } : {} });
40492
+ if (opts.cron) {
40493
+ try {
40494
+ new Cron2(opts.cron).nextRun();
40495
+ } catch {
40496
+ throw new Error(`invalid --cron expression: ${JSON.stringify(opts.cron)}`);
40497
+ }
40498
+ if (opts.tz) {
40499
+ try {
40500
+ new Cron2(opts.cron, { timezone: opts.tz }).nextRun();
40501
+ } catch {
40502
+ throw new Error(`invalid --tz timezone: ${JSON.stringify(opts.tz)}`);
40503
+ }
40504
+ }
40505
+ chosen.push({ kind: "cron", expr: opts.cron, ...opts.tz ? { tz: opts.tz } : {} });
40506
+ }
40392
40507
  if (opts.afterReset) {
40393
40508
  const provider = opts.afterReset;
40394
40509
  if (provider !== "claude" && provider !== "codex") {
@@ -40442,7 +40557,13 @@ scheduleCommand.command("create").description("Create a scheduled job").required
40442
40557
  if (unattended) assertUnattendedTerminalSupported(terminal);
40443
40558
  const limits = defaultLimits();
40444
40559
  if (opts.maxRuntime) limits.maxRuntimeMs = parseDurationMs(opts.maxRuntime);
40445
- if (opts.maxLaunchesPerDay) limits.maxLaunchesPerDay = Number.parseInt(opts.maxLaunchesPerDay, 10);
40560
+ if (opts.maxLaunchesPerDay) {
40561
+ const n = Number(opts.maxLaunchesPerDay);
40562
+ if (!Number.isInteger(n) || n <= 0) {
40563
+ throw new Error("--max-launches-per-day must be a positive integer");
40564
+ }
40565
+ limits.maxLaunchesPerDay = n;
40566
+ }
40446
40567
  if (opts.cooldown) limits.cooldownMs = parseDurationMs(opts.cooldown);
40447
40568
  const now = nowTimestamp();
40448
40569
  const job = {