syntaur 0.57.0 → 0.58.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 (66) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dashboard/dist/assets/{_basePickBy-JSKTFkEa.js → _basePickBy-BSxL8bBc.js} +1 -1
  3. package/dashboard/dist/assets/{_baseUniq-B4THfRv6.js → _baseUniq-1f9ohhNp.js} +1 -1
  4. package/dashboard/dist/assets/{arc-BT5oCPaC.js → arc-DlPK9j9g.js} +1 -1
  5. package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-C0wRHwPD.js → architectureDiagram-2XIMDMQ5-CcV5llaa.js} +1 -1
  6. package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-yMj3_K19.js → blockDiagram-WCTKOSBZ-DGkYzINJ.js} +1 -1
  7. package/dashboard/dist/assets/{c4Diagram-IC4MRINW-6fr_jElW.js → c4Diagram-IC4MRINW-CI4Z5c6N.js} +1 -1
  8. package/dashboard/dist/assets/channel-Qr2eMXkR.js +1 -0
  9. package/dashboard/dist/assets/{chunk-4BX2VUAB-BS0AdM9N.js → chunk-4BX2VUAB-C8KasqJp.js} +1 -1
  10. package/dashboard/dist/assets/{chunk-55IACEB6-Bf3cMgj4.js → chunk-55IACEB6-CgAEilyk.js} +1 -1
  11. package/dashboard/dist/assets/{chunk-FMBD7UC4-BxhOy53m.js → chunk-FMBD7UC4-DsNiby6p.js} +1 -1
  12. package/dashboard/dist/assets/{chunk-JSJVCQXG-Da4w_8jA.js → chunk-JSJVCQXG-4eCDFRIx.js} +1 -1
  13. package/dashboard/dist/assets/{chunk-KX2RTZJC-p3BnTwvF.js → chunk-KX2RTZJC-DHia_zig.js} +1 -1
  14. package/dashboard/dist/assets/{chunk-NQ4KR5QH-DfvY-FfQ.js → chunk-NQ4KR5QH-V-IiQwer.js} +1 -1
  15. package/dashboard/dist/assets/{chunk-QZHKN3VN-BY_yeD01.js → chunk-QZHKN3VN-BXTS8E6J.js} +1 -1
  16. package/dashboard/dist/assets/{chunk-WL4C6EOR-yQKlkPsr.js → chunk-WL4C6EOR-BuLlUWn0.js} +1 -1
  17. package/dashboard/dist/assets/classDiagram-VBA2DB6C-DHkm1twH.js +1 -0
  18. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-DHkm1twH.js +1 -0
  19. package/dashboard/dist/assets/clone-ChZeNkMI.js +1 -0
  20. package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-CDoLPu4r.js → cose-bilkent-S5V4N54A-B1M1ENTo.js} +1 -1
  21. package/dashboard/dist/assets/{dagre-KLK3FWXG-BsEV2YI1.js → dagre-KLK3FWXG-3pyNBzWY.js} +1 -1
  22. package/dashboard/dist/assets/{diagram-E7M64L7V-CF2uTCw-.js → diagram-E7M64L7V-B5-5pbKh.js} +1 -1
  23. package/dashboard/dist/assets/{diagram-IFDJBPK2-CCbV7q-H.js → diagram-IFDJBPK2-CTx5NFFi.js} +1 -1
  24. package/dashboard/dist/assets/{diagram-P4PSJMXO-C-hmCZ82.js → diagram-P4PSJMXO-B5KRvZqb.js} +1 -1
  25. package/dashboard/dist/assets/{erDiagram-INFDFZHY-Bqmd6M31.js → erDiagram-INFDFZHY-BhVWsRrc.js} +1 -1
  26. package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-BoHMjR8s.js → flowDiagram-PKNHOUZH-CuoB0thO.js} +1 -1
  27. package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-Bwi2K6t0.js → ganttDiagram-A5KZAMGK-yNNalQY9.js} +1 -1
  28. package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-BLg_41tI.js → gitGraphDiagram-K3NZZRJ6-DfEhHzOH.js} +1 -1
  29. package/dashboard/dist/assets/{graph-D5SqATbG.js → graph-NaP7lLuY.js} +1 -1
  30. package/dashboard/dist/assets/index-BG_Ed2-k.js +644 -0
  31. package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-MQGu_WQh.js → infoDiagram-LFFYTUFH-CDAMAxDE.js} +1 -1
  32. package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-Cq2VOOGw.js → ishikawaDiagram-PHBUUO56-DMRl5xeW.js} +1 -1
  33. package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-DMgBiVbw.js → journeyDiagram-4ABVD52K-DtuQQjW_.js} +1 -1
  34. package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-CW553HfT.js → kanban-definition-K7BYSVSG-DYGt44vU.js} +1 -1
  35. package/dashboard/dist/assets/{layout-CY_IT3u3.js → layout-B9sS0Gu9.js} +1 -1
  36. package/dashboard/dist/assets/{linear-BwR3oULZ.js → linear-DQcNFWB2.js} +1 -1
  37. package/dashboard/dist/assets/{mermaid.core-CmEGzmWo.js → mermaid.core-CzhsZ1QZ.js} +4 -4
  38. package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-D__wNWPY.js → mindmap-definition-YRQLILUH-CL8mKZYJ.js} +1 -1
  39. package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-BsFl-giQ.js → pieDiagram-SKSYHLDU-BULUJrhI.js} +1 -1
  40. package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-C-F2YU1C.js → quadrantDiagram-337W2JSQ-CWQC3mf-.js} +1 -1
  41. package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-C0_TmWRh.js → requirementDiagram-Z7DCOOCP-CYadvMn7.js} +1 -1
  42. package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-DpoaSHiD.js → sankeyDiagram-WA2Y5GQK-e2AhGJiF.js} +1 -1
  43. package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-DBb9oieZ.js → sequenceDiagram-2WXFIKYE-CeWKKFdg.js} +1 -1
  44. package/dashboard/dist/assets/{stateDiagram-RAJIS63D-Cw5xaaAs.js → stateDiagram-RAJIS63D-BvN-wCps.js} +1 -1
  45. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-BV7kxiIj.js +1 -0
  46. package/dashboard/dist/assets/{timeline-definition-YZTLITO2-rOhPVhqR.js → timeline-definition-YZTLITO2-DOLRfgbL.js} +1 -1
  47. package/dashboard/dist/assets/{treemap-KZPCXAKY-CD-TOoiS.js → treemap-KZPCXAKY-VqhsqWzF.js} +1 -1
  48. package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-BH78k9Wk.js → vennDiagram-LZ73GAT5-Dt-knC6b.js} +1 -1
  49. package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-1hqiJxk_.js → xychartDiagram-JWTSCODW-A3enOtVP.js} +1 -1
  50. package/dashboard/dist/index.html +1 -1
  51. package/dist/dashboard/server.js +208 -131
  52. package/dist/dashboard/server.js.map +1 -1
  53. package/dist/index.js +584 -591
  54. package/dist/index.js.map +1 -1
  55. package/dist/launch/index.js.map +1 -1
  56. package/package.json +1 -1
  57. package/platforms/claude-code/.claude-plugin/plugin.json +1 -1
  58. package/platforms/codex/.codex-plugin/plugin.json +1 -1
  59. package/platforms/hermes/plugins/syntaur/__pycache__/__init__.cpython-312.pyc +0 -0
  60. package/platforms/hermes/plugins/syntaur/__pycache__/boundary.cpython-312.pyc +0 -0
  61. package/dashboard/dist/assets/channel-BZWZQgJF.js +0 -1
  62. package/dashboard/dist/assets/classDiagram-VBA2DB6C-obLZNHbe.js +0 -1
  63. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-obLZNHbe.js +0 -1
  64. package/dashboard/dist/assets/clone-gWuoHRfO.js +0 -1
  65. package/dashboard/dist/assets/index-CQaTQf_I.js +0 -639
  66. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-BpeHnq6R.js +0 -1
@@ -1271,6 +1271,7 @@ __export(parser_exports, {
1271
1271
  readLog: () => readLog,
1272
1272
  serializeChecklist: () => serializeChecklist,
1273
1273
  serializeChecklistItem: () => serializeChecklistItem,
1274
+ serializeLog: () => serializeLog,
1274
1275
  serializeLogEntry: () => serializeLogEntry,
1275
1276
  serializeMetaToken: () => serializeMetaToken,
1276
1277
  writeChecklist: () => writeChecklist
@@ -1398,27 +1399,60 @@ function statusToMarker(item) {
1398
1399
  return item.session ? `>:${sanitizeSession(item.session)}` : ">";
1399
1400
  }
1400
1401
  }
1402
+ function escapeDescription(description) {
1403
+ return description.replace(/\\/g, "\\\\").replace(/#/g, "\\#").replace(/\[/g, "\\[");
1404
+ }
1405
+ function unescapeDescription(escaped) {
1406
+ let out = "";
1407
+ for (let i = 0; i < escaped.length; i++) {
1408
+ if (escaped[i] === "\\" && i + 1 < escaped.length) {
1409
+ const next = escaped[i + 1];
1410
+ if (next === "\\" || next === "#" || next === "[") {
1411
+ out += next;
1412
+ i++;
1413
+ continue;
1414
+ }
1415
+ }
1416
+ out += escaped[i];
1417
+ }
1418
+ return out;
1419
+ }
1420
+ function findStructuralCut(rest) {
1421
+ for (let i = 0; i < rest.length; i++) {
1422
+ const ch = rest[i];
1423
+ if (ch !== "#" && ch !== "[") continue;
1424
+ let backslashes = 0;
1425
+ let j = i - 1;
1426
+ while (j >= 0 && rest[j] === "\\") {
1427
+ backslashes++;
1428
+ j--;
1429
+ }
1430
+ if (backslashes % 2 === 1) continue;
1431
+ if (ch === "#") {
1432
+ if (/[a-zA-Z0-9_-]/.test(rest[i + 1] ?? "")) return i;
1433
+ } else {
1434
+ return i;
1435
+ }
1436
+ }
1437
+ return rest.length;
1438
+ }
1401
1439
  function parseChecklistItem(line) {
1402
1440
  const match = line.match(ITEM_REGEX);
1403
1441
  if (!match) return null;
1404
1442
  const marker = match[1];
1405
1443
  const rest = match[2];
1406
1444
  const { status, session } = parseStatus2(marker);
1407
- const idMatch = rest.match(ID_REGEX);
1445
+ const cut = findStructuralCut(rest);
1446
+ const description = unescapeDescription(rest.slice(0, cut).trim());
1447
+ const tail = rest.slice(cut);
1448
+ const idMatch = tail.match(ID_REGEX);
1408
1449
  const id = idMatch ? idMatch[1] : "";
1409
1450
  const tags = [];
1410
1451
  let tagMatch;
1411
1452
  const tagRegex = new RegExp(TAG_REGEX.source, "g");
1412
- while ((tagMatch = tagRegex.exec(rest)) !== null) {
1453
+ while ((tagMatch = tagRegex.exec(tail)) !== null) {
1413
1454
  tags.push(tagMatch[1]);
1414
1455
  }
1415
- let description = rest;
1416
- const firstTagIdx = rest.search(/#[a-zA-Z0-9_-]/);
1417
- const firstIdIdx = rest.search(/\[t:[a-f0-9]{4}\]/);
1418
- const cutPoints = [firstTagIdx, firstIdIdx].filter((i) => i >= 0);
1419
- if (cutPoints.length > 0) {
1420
- description = rest.slice(0, Math.min(...cutPoints)).trim();
1421
- }
1422
1456
  const meta = parseMetaToken(line);
1423
1457
  return {
1424
1458
  id,
@@ -1439,7 +1473,7 @@ function parseChecklistItem(line) {
1439
1473
  function serializeChecklistItem(item) {
1440
1474
  const marker = statusToMarker(item);
1441
1475
  const tagStr = item.tags.map((t) => `#${t}`).join(" ");
1442
- const parts = [`- [${marker}] ${item.description}`];
1476
+ const parts = [`- [${marker}] ${escapeDescription(item.description)}`];
1443
1477
  if (tagStr) parts.push(tagStr);
1444
1478
  parts.push(`[t:${item.id}]`);
1445
1479
  const meta = serializeMetaToken(item);
@@ -1538,6 +1572,18 @@ function serializeLogEntry(entry) {
1538
1572
  if (entry.status) lines.push(`**Status:** ${entry.status}`);
1539
1573
  return lines.join("\n");
1540
1574
  }
1575
+ function serializeLog(log) {
1576
+ const header = `---
1577
+ workspace: ${log.workspace}
1578
+ ---
1579
+
1580
+ # Todo Log
1581
+ `;
1582
+ if (log.entries.length === 0) {
1583
+ return header;
1584
+ }
1585
+ return header + "\n" + log.entries.map(serializeLogEntry).join("\n\n") + "\n";
1586
+ }
1541
1587
  function checklistPath(todosDir2, workspace) {
1542
1588
  return resolve2(todosDir2, `${workspace}.md`);
1543
1589
  }
@@ -14274,7 +14320,7 @@ ${nextBody}${nextBody.endsWith("\n") ? "" : "\n"}`;
14274
14320
  };
14275
14321
  const entry = formatCommentEntry(comment);
14276
14322
  let next = setTopLevelField(currentContent, "entryCount", String(currentCount + 1));
14277
- next = setTopLevelField(next, "updated", `"${timestamp}"`);
14323
+ next = setTopLevelField(next, "updated", timestamp);
14278
14324
  if (next.includes("No comments yet.")) {
14279
14325
  next = next.replace("No comments yet.", entry.trimEnd());
14280
14326
  } else {
@@ -14334,7 +14380,7 @@ ${entry}`;
14334
14380
  res.status(500).json({ error: "Failed to update resolved flag" });
14335
14381
  return;
14336
14382
  }
14337
- const withUpdated = setTopLevelField(next, "updated", `"${nowTimestamp()}"`);
14383
+ const withUpdated = setTopLevelField(next, "updated", nowTimestamp());
14338
14384
  await writeFileForce(commentsPath, withUpdated);
14339
14385
  const assignment = await getAssignmentDetail(projectsDir, projectSlug, assignmentSlug);
14340
14386
  res.json({ assignment });
@@ -15803,7 +15849,7 @@ async function appendCommentTo(assignmentDir, assignmentRef, req2, res, reloadDe
15803
15849
  };
15804
15850
  const entry = formatCommentEntry(comment);
15805
15851
  let next = setTopLevelField(currentContent, "entryCount", String(currentCount + 1));
15806
- next = setTopLevelField(next, "updated", `"${timestamp}"`);
15852
+ next = setTopLevelField(next, "updated", timestamp);
15807
15853
  if (next.includes("No comments yet.")) {
15808
15854
  next = next.replace("No comments yet.", entry.trimEnd());
15809
15855
  } else {
@@ -15846,7 +15892,7 @@ async function toggleCommentResolvedAt(assignmentDir, commentId, req2, res, relo
15846
15892
  res.status(500).json({ error: "Failed to update resolved flag" });
15847
15893
  return;
15848
15894
  }
15849
- const withUpdated = setTopLevelField(next, "updated", `"${nowTimestamp()}"`);
15895
+ const withUpdated = setTopLevelField(next, "updated", nowTimestamp());
15850
15896
  await writeFileForce(commentsPath, withUpdated);
15851
15897
  const assignment = await reloadDetail();
15852
15898
  res.json({ assignment });
@@ -18657,6 +18703,119 @@ async function readEvents(jobId) {
18657
18703
  import { createHash as createHash3 } from "crypto";
18658
18704
  import { open as open4, readFile as readFile21, stat as stat3, unlink as unlink6 } from "fs/promises";
18659
18705
  import { resolve as resolve29 } from "path";
18706
+
18707
+ // src/schedules/triggers.ts
18708
+ import { Cron } from "croner";
18709
+
18710
+ // src/schedules/reset-window.ts
18711
+ var FIVE_HOURS_MS = 5 * 60 * 60 * 1e3;
18712
+ var ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1e3;
18713
+ function windowLengthMs(anchor) {
18714
+ return anchor.windowKind === "weekly" ? ONE_WEEK_MS : FIVE_HOURS_MS;
18715
+ }
18716
+ function predictReset(_provider, anchor) {
18717
+ const start = Date.parse(anchor.windowStartIso);
18718
+ if (Number.isNaN(start)) {
18719
+ throw new Error(`Invalid reset anchor windowStartIso: ${anchor.windowStartIso}`);
18720
+ }
18721
+ return new Date(start + windowLengthMs(anchor)).toISOString().replace(/\.\d{3}Z$/, "Z");
18722
+ }
18723
+ function verifyReset(provider, anchor, now) {
18724
+ const reset = predictReset(provider, anchor);
18725
+ if (now.getTime() >= Date.parse(reset)) return { eligible: true };
18726
+ return { eligible: false, rescheduleToIso: reset };
18727
+ }
18728
+
18729
+ // src/schedules/triggers.ts
18730
+ function iso(d) {
18731
+ return d.toISOString().replace(/\.\d{3}Z$/, "Z");
18732
+ }
18733
+ function isRecurring(trigger) {
18734
+ return trigger.kind === "cron";
18735
+ }
18736
+ var notDue = { due: false };
18737
+ function evaluateTrigger(job, ctx) {
18738
+ const consumed = new Set(job.attempt.consumedEdges);
18739
+ const result = evaluateKind(job.trigger, job, ctx);
18740
+ if (result.due && result.dedupeKey && consumed.has(result.dedupeKey)) {
18741
+ return { ...result, due: false };
18742
+ }
18743
+ return result;
18744
+ }
18745
+ function evaluateKind(trigger, job, ctx) {
18746
+ const createdAtMs = Date.parse(job.createdAt);
18747
+ switch (trigger.kind) {
18748
+ case "at": {
18749
+ const at = Date.parse(trigger.at);
18750
+ const due = !Number.isNaN(at) && ctx.now.getTime() >= at;
18751
+ return { due, dedupeKey: `at:${trigger.at}`, nextFireIso: trigger.at };
18752
+ }
18753
+ case "in": {
18754
+ const anchor = Date.parse(trigger.anchorIso);
18755
+ const fireAt = anchor + trigger.durationMs;
18756
+ const due = !Number.isNaN(anchor) && ctx.now.getTime() >= fireAt;
18757
+ const fireIso = Number.isNaN(anchor) ? null : iso(new Date(fireAt));
18758
+ return { due, dedupeKey: `in:${fireAt}`, nextFireIso: fireIso };
18759
+ }
18760
+ case "cron":
18761
+ return evaluateCron(trigger, ctx.now, createdAtMs);
18762
+ case "after-reset":
18763
+ return evaluateAfterReset(trigger, ctx.now);
18764
+ case "when-status":
18765
+ return evaluateWhenStatus(trigger, job, ctx, createdAtMs);
18766
+ case "when-plan-lands":
18767
+ return evaluateWhenPlanLands(ctx, createdAtMs);
18768
+ }
18769
+ }
18770
+ function evaluateCron(trigger, now, createdAtMs) {
18771
+ let cron;
18772
+ try {
18773
+ cron = new Cron(trigger.expr, trigger.tz ? { timezone: trigger.tz } : {});
18774
+ } catch {
18775
+ return notDue;
18776
+ }
18777
+ const prevs = cron.previousRuns(1, new Date(now.getTime() + 1e3));
18778
+ let prev = prevs.length > 0 ? prevs[0] : null;
18779
+ if (prev && prev.getTime() > now.getTime()) prev = null;
18780
+ if (prev && !Number.isNaN(createdAtMs) && prev.getTime() < createdAtMs) prev = null;
18781
+ const next = cron.nextRun(now);
18782
+ if (!prev) return { due: false, nextFireIso: next ? iso(next) : null };
18783
+ return { due: true, dedupeKey: `cron:${iso(prev)}`, nextFireIso: next ? iso(next) : null };
18784
+ }
18785
+ function evaluateAfterReset(trigger, now) {
18786
+ const v = verifyReset(trigger.provider, trigger.anchor, now);
18787
+ const predicted = predictReset(trigger.provider, trigger.anchor);
18788
+ if (v.eligible) {
18789
+ return { due: true, dedupeKey: `after-reset:${trigger.anchor.windowStartIso}`, nextFireIso: predicted };
18790
+ }
18791
+ return { due: false, nextFireIso: predicted, rescheduleToIso: v.rescheduleToIso };
18792
+ }
18793
+ function evaluateWhenStatus(trigger, job, ctx, createdAtMs) {
18794
+ const fm = ctx.assignment;
18795
+ if (!fm) return notDue;
18796
+ const history = fm.statusHistory ?? [];
18797
+ for (let i = job.attempt.cursor; i < history.length; i++) {
18798
+ const atMs = Date.parse(history[i].at);
18799
+ const afterCreation = Number.isNaN(createdAtMs) || Number.isNaN(atMs) || atMs >= createdAtMs;
18800
+ if (history[i].to === trigger.status && afterCreation) {
18801
+ return { due: true, dedupeKey: `status:${i}:${history[i].at}`, nextCursor: i + 1 };
18802
+ }
18803
+ }
18804
+ return { due: false, nextCursor: history.length };
18805
+ }
18806
+ function evaluateWhenPlanLands(ctx, createdAtMs) {
18807
+ const fm = ctx.assignment;
18808
+ if (!fm) return notDue;
18809
+ const approval = fm.planApproval;
18810
+ if (!approval) return notDue;
18811
+ const approvedMs = Date.parse(approval.at);
18812
+ if (!Number.isNaN(createdAtMs) && !Number.isNaN(approvedMs) && approvedMs < createdAtMs) {
18813
+ return notDue;
18814
+ }
18815
+ return { due: true, dedupeKey: `plan:${approval.file}:${approval.digest}` };
18816
+ }
18817
+
18818
+ // src/schedules/attempt.ts
18660
18819
  init_timestamp();
18661
18820
 
18662
18821
  // src/schedules/types.ts
@@ -18855,6 +19014,15 @@ async function markLaunchFailed(job, reason) {
18855
19014
  await appendEvent(job.id, "launch_failed", { reason });
18856
19015
  return written;
18857
19016
  }
19017
+ async function markCompleted(job) {
19018
+ const next = {
19019
+ ...job,
19020
+ attempt: { ...job.attempt, state: "completed", claim: null }
19021
+ };
19022
+ const written = await writeJob(next);
19023
+ await appendEvent(job.id, "completed", { reason: "one-shot session ended" });
19024
+ return written;
19025
+ }
18858
19026
  var TransitionError = class extends Error {
18859
19027
  constructor(message) {
18860
19028
  super(message);
@@ -18942,10 +19110,11 @@ function retryJob(id) {
18942
19110
  () => ({ claim: null, lastError: null })
18943
19111
  );
18944
19112
  }
19113
+ var ONE_SHOT_COMPLETE_GRACE_MS = 6e4;
18945
19114
  async function reapStale(jobs, deps) {
18946
19115
  const now = deps.now();
18947
19116
  const isLive = deps.isSessionLive ?? (() => true);
18948
- const out = { reaped: [], stuck: [] };
19117
+ const out = { reaped: [], stuck: [], completed: [] };
18949
19118
  for (const job of jobs) {
18950
19119
  const a = job.attempt;
18951
19120
  if ((a.state === "claimed" || a.state === "launching") && a.claim && now.getTime() > a.claim.expiresAt) {
@@ -18954,7 +19123,17 @@ async function reapStale(jobs, deps) {
18954
19123
  out.reaped.push(job.id);
18955
19124
  continue;
18956
19125
  }
18957
- if (a.state === "running" && job.limits.maxRuntimeMs && a.runningSince) {
19126
+ if (a.state !== "running") continue;
19127
+ if (!isRecurring(job.trigger) && a.sessionId && a.runningSince) {
19128
+ const graceMs = job.limits.maxRuntimeMs ?? ONE_SHOT_COMPLETE_GRACE_MS;
19129
+ const pastGrace = now.getTime() - Date.parse(a.runningSince) > graceMs;
19130
+ if (pastGrace && !isLive(a.sessionId, a.launchPid)) {
19131
+ await markCompleted(job);
19132
+ out.completed.push(job.id);
19133
+ continue;
19134
+ }
19135
+ }
19136
+ if (job.limits.maxRuntimeMs && a.runningSince) {
18958
19137
  const overrun = now.getTime() - Date.parse(a.runningSince) > job.limits.maxRuntimeMs;
18959
19138
  if (overrun && !isLive(a.sessionId, a.launchPid) && a.lastError !== "stuck:max-runtime") {
18960
19139
  await writeJob({ ...job, attempt: { ...a, lastError: "stuck:max-runtime" } });
@@ -19138,117 +19317,6 @@ init_frontmatter();
19138
19317
  import { readFile as readFile22 } from "fs/promises";
19139
19318
  import { resolve as resolve31 } from "path";
19140
19319
 
19141
- // src/schedules/triggers.ts
19142
- import { Cron } from "croner";
19143
-
19144
- // src/schedules/reset-window.ts
19145
- var FIVE_HOURS_MS = 5 * 60 * 60 * 1e3;
19146
- var ONE_WEEK_MS = 7 * 24 * 60 * 60 * 1e3;
19147
- function windowLengthMs(anchor) {
19148
- return anchor.windowKind === "weekly" ? ONE_WEEK_MS : FIVE_HOURS_MS;
19149
- }
19150
- function predictReset(_provider, anchor) {
19151
- const start = Date.parse(anchor.windowStartIso);
19152
- if (Number.isNaN(start)) {
19153
- throw new Error(`Invalid reset anchor windowStartIso: ${anchor.windowStartIso}`);
19154
- }
19155
- return new Date(start + windowLengthMs(anchor)).toISOString().replace(/\.\d{3}Z$/, "Z");
19156
- }
19157
- function verifyReset(provider, anchor, now) {
19158
- const reset = predictReset(provider, anchor);
19159
- if (now.getTime() >= Date.parse(reset)) return { eligible: true };
19160
- return { eligible: false, rescheduleToIso: reset };
19161
- }
19162
-
19163
- // src/schedules/triggers.ts
19164
- function iso(d) {
19165
- return d.toISOString().replace(/\.\d{3}Z$/, "Z");
19166
- }
19167
- function isRecurring(trigger) {
19168
- return trigger.kind === "cron";
19169
- }
19170
- var notDue = { due: false };
19171
- function evaluateTrigger(job, ctx) {
19172
- const consumed = new Set(job.attempt.consumedEdges);
19173
- const result = evaluateKind(job.trigger, job, ctx);
19174
- if (result.due && result.dedupeKey && consumed.has(result.dedupeKey)) {
19175
- return { ...result, due: false };
19176
- }
19177
- return result;
19178
- }
19179
- function evaluateKind(trigger, job, ctx) {
19180
- const createdAtMs = Date.parse(job.createdAt);
19181
- switch (trigger.kind) {
19182
- case "at": {
19183
- const at = Date.parse(trigger.at);
19184
- const due = !Number.isNaN(at) && ctx.now.getTime() >= at;
19185
- return { due, dedupeKey: `at:${trigger.at}`, nextFireIso: trigger.at };
19186
- }
19187
- case "in": {
19188
- const anchor = Date.parse(trigger.anchorIso);
19189
- const fireAt = anchor + trigger.durationMs;
19190
- const due = !Number.isNaN(anchor) && ctx.now.getTime() >= fireAt;
19191
- const fireIso = Number.isNaN(anchor) ? null : iso(new Date(fireAt));
19192
- return { due, dedupeKey: `in:${fireAt}`, nextFireIso: fireIso };
19193
- }
19194
- case "cron":
19195
- return evaluateCron(trigger, ctx.now, createdAtMs);
19196
- case "after-reset":
19197
- return evaluateAfterReset(trigger, ctx.now);
19198
- case "when-status":
19199
- return evaluateWhenStatus(trigger, job, ctx, createdAtMs);
19200
- case "when-plan-lands":
19201
- return evaluateWhenPlanLands(ctx, createdAtMs);
19202
- }
19203
- }
19204
- function evaluateCron(trigger, now, createdAtMs) {
19205
- let cron;
19206
- try {
19207
- cron = new Cron(trigger.expr, trigger.tz ? { timezone: trigger.tz } : {});
19208
- } catch {
19209
- return notDue;
19210
- }
19211
- const prevs = cron.previousRuns(1, new Date(now.getTime() + 1e3));
19212
- let prev = prevs.length > 0 ? prevs[0] : null;
19213
- if (prev && prev.getTime() > now.getTime()) prev = null;
19214
- if (prev && !Number.isNaN(createdAtMs) && prev.getTime() < createdAtMs) prev = null;
19215
- const next = cron.nextRun(now);
19216
- if (!prev) return { due: false, nextFireIso: next ? iso(next) : null };
19217
- return { due: true, dedupeKey: `cron:${iso(prev)}`, nextFireIso: next ? iso(next) : null };
19218
- }
19219
- function evaluateAfterReset(trigger, now) {
19220
- const v = verifyReset(trigger.provider, trigger.anchor, now);
19221
- const predicted = predictReset(trigger.provider, trigger.anchor);
19222
- if (v.eligible) {
19223
- return { due: true, dedupeKey: `after-reset:${trigger.anchor.windowStartIso}`, nextFireIso: predicted };
19224
- }
19225
- return { due: false, nextFireIso: predicted, rescheduleToIso: v.rescheduleToIso };
19226
- }
19227
- function evaluateWhenStatus(trigger, job, ctx, createdAtMs) {
19228
- const fm = ctx.assignment;
19229
- if (!fm) return notDue;
19230
- const history = fm.statusHistory ?? [];
19231
- for (let i = job.attempt.cursor; i < history.length; i++) {
19232
- const atMs = Date.parse(history[i].at);
19233
- const afterCreation = Number.isNaN(createdAtMs) || Number.isNaN(atMs) || atMs >= createdAtMs;
19234
- if (history[i].to === trigger.status && afterCreation) {
19235
- return { due: true, dedupeKey: `status:${i}:${history[i].at}`, nextCursor: i + 1 };
19236
- }
19237
- }
19238
- return { due: false, nextCursor: history.length };
19239
- }
19240
- function evaluateWhenPlanLands(ctx, createdAtMs) {
19241
- const fm = ctx.assignment;
19242
- if (!fm) return notDue;
19243
- const approval = fm.planApproval;
19244
- if (!approval) return notDue;
19245
- const approvedMs = Date.parse(approval.at);
19246
- if (!Number.isNaN(createdAtMs) && !Number.isNaN(approvedMs) && approvedMs < createdAtMs) {
19247
- return notDue;
19248
- }
19249
- return { due: true, dedupeKey: `plan:${approval.file}:${approval.digest}` };
19250
- }
19251
-
19252
19320
  // src/schedules/launch-ack.ts
19253
19321
  init_session_id();
19254
19322
  import { readdirSync } from "fs";
@@ -19330,11 +19398,12 @@ async function defaultResolvePlan(job) {
19330
19398
  async function runTick(deps = {}) {
19331
19399
  const now = deps.now ?? (() => /* @__PURE__ */ new Date());
19332
19400
  const attemptDeps = { now, isSessionLive: deps.isSessionLive };
19333
- const result = { evaluated: 0, fired: [], failed: [], skipped: 0, reaped: [], stuck: [] };
19401
+ const result = { evaluated: 0, fired: [], failed: [], skipped: 0, reaped: [], stuck: [], completed: [] };
19334
19402
  if (deps.reap !== false) {
19335
19403
  const reap = await reapStale(await listJobs(), attemptDeps);
19336
19404
  result.reaped = reap.reaped;
19337
19405
  result.stuck = reap.stuck;
19406
+ result.completed = reap.completed;
19338
19407
  }
19339
19408
  const jobs = await listJobs();
19340
19409
  for (const job of jobs) {
@@ -20999,6 +21068,9 @@ workspace: ${workspace}
20999
21068
  await writeFileForce2(archFile, archContent);
21000
21069
  checklist.items = checklist.items.filter((i) => !completedIds.has(i.id));
21001
21070
  await writeChecklist(todosDir2, checklist);
21071
+ const archivedEntries = new Set(toArchive);
21072
+ log.entries = log.entries.filter((e) => !archivedEntries.has(e));
21073
+ await writeFileForce2(logPath(todosDir2, workspace), serializeLog(log));
21002
21074
  for (const id of completedIds) {
21003
21075
  await deleteAllAttachments(todosDir2, workspace, id);
21004
21076
  }
@@ -21743,6 +21815,9 @@ workspace: ${slug}
21743
21815
  checklist.workspace = slug;
21744
21816
  checklist.items = checklist.items.filter((i) => !completedIds.has(i.id));
21745
21817
  await writeChecklist(todosDir2, checklist);
21818
+ const archivedEntries = new Set(toArchive);
21819
+ log.entries = log.entries.filter((e) => !archivedEntries.has(e));
21820
+ await writeFileForce(logPath(todosDir2, slug), serializeLog(log));
21746
21821
  for (const id of completedIds) {
21747
21822
  await deleteAllAttachments(todosDir2, slug, id);
21748
21823
  }
@@ -24355,9 +24430,11 @@ function createDashboardServer(options) {
24355
24430
  return new Promise((resolvePromise, reject) => {
24356
24431
  server.on("error", (err) => {
24357
24432
  if (err.code === "EADDRINUSE") {
24358
- reject(new Error(
24359
- `Port ${port} is already in use. Use --port <number> to specify a different port.`
24360
- ));
24433
+ const inUse = new Error(
24434
+ `Port ${port} is already in use.`
24435
+ );
24436
+ inUse.code = "EADDRINUSE";
24437
+ reject(inUse);
24361
24438
  } else {
24362
24439
  reject(err);
24363
24440
  }