@lchase/kiln 0.9.0 → 0.11.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.
package/assets/serve.js CHANGED
@@ -605,6 +605,8 @@
605
605
  base: repo.base || "",
606
606
  prTitle: repo.prTitle || "",
607
607
  branch: repo.branch || "",
608
+ maxAttempts: repo.maxAttempts != null ? String(repo.maxAttempts) : "",
609
+ maxFixRounds: repo.maxFixRounds != null ? String(repo.maxFixRounds) : "",
608
610
  };
609
611
  }
610
612
  return out;
@@ -625,7 +627,11 @@
625
627
  var attempts = document.getElementById("hint-attempts");
626
628
  var stall = document.getElementById("hint-stall");
627
629
  var rounds = document.getElementById("hint-fix");
628
- if (attempts) attempts.textContent = "Default " + (settingsDefaults.maxAttempts != null ? settingsDefaults.maxAttempts : 3) + ". Transient failures before the job is dead.";
630
+ if (attempts) attempts.textContent = "Default " + (settingsDefaults.maxAttempts != null ? settingsDefaults.maxAttempts : 3) + ". Transient failures before the job needs a person.";
631
+ var fingerprint = document.getElementById("hint-fingerprint");
632
+ var cap = document.getElementById("hint-cap");
633
+ if (fingerprint) fingerprint.textContent = "Default " + (settingsDefaults.maxSameFingerprint != null ? settingsDefaults.maxSameFingerprint : 3) + ". Identical fingerprints before the job needs a person.";
634
+ if (cap) cap.textContent = "Empty means no daily cap. Recorded dollars, local day.";
629
635
  if (stall) stall.textContent = "Default " + (settingsDefaults.stallTimeoutMs != null ? settingsDefaults.stallTimeoutMs : 900000) + " (15 minutes). 0 disables stall detection.";
630
636
  if (rounds) rounds.textContent = "Default " + (settingsDefaults.maxFixRounds != null ? settingsDefaults.maxFixRounds : 2) + ". Fix rounds before the job needs a person.";
631
637
  var attemptInput = document.getElementById("max-attempts");
@@ -634,6 +640,8 @@
634
640
  if (attemptInput) attemptInput.placeholder = String(settingsDefaults.maxAttempts != null ? settingsDefaults.maxAttempts : 3);
635
641
  if (stallInput) stallInput.placeholder = String(settingsDefaults.stallTimeoutMs != null ? settingsDefaults.stallTimeoutMs : 900000);
636
642
  if (roundInput) roundInput.placeholder = String(settingsDefaults.maxFixRounds != null ? settingsDefaults.maxFixRounds : 2);
643
+ var fingerprintInput = document.getElementById("max-same-fingerprint");
644
+ if (fingerprintInput) fingerprintInput.placeholder = String(settingsDefaults.maxSameFingerprint != null ? settingsDefaults.maxSameFingerprint : 3);
637
645
  }
638
646
 
639
647
  function fillGlobal(data) {
@@ -644,6 +652,10 @@
644
652
  if (attempts) attempts.value = data.maxAttempts != null ? String(data.maxAttempts) : "";
645
653
  if (stall) stall.value = data.stallTimeoutMs != null ? String(data.stallTimeoutMs) : "";
646
654
  if (rounds) rounds.value = data.maxFixRounds != null ? String(data.maxFixRounds) : "";
655
+ var fingerprint = document.getElementById("max-same-fingerprint");
656
+ var cap = document.getElementById("daily-dollar-cap");
657
+ if (fingerprint) fingerprint.value = data.maxSameFingerprint != null ? String(data.maxSameFingerprint) : "";
658
+ if (cap) cap.value = data.dailyDollarCap != null ? String(data.dailyDollarCap) : "";
647
659
  setPressed("#sandbox-mode [data-sandbox]", "data-sandbox", data.sandbox && data.sandbox.mode === "docker" ? "docker" : "off");
648
660
  var domains = document.getElementById("sandbox-domains");
649
661
  if (domains) domains.value = data.sandbox && data.sandbox.allowDomains ? data.sandbox.allowDomains.join("\n") : "";
@@ -674,12 +686,16 @@
674
686
  var baseInput = document.getElementById("repo-base");
675
687
  var titleInput = document.getElementById("repo-pr-title");
676
688
  var branchInput = document.getElementById("repo-branch");
689
+ var attemptsInput = document.getElementById("repo-max-attempts");
690
+ var roundsInput = document.getElementById("repo-max-fix-rounds");
677
691
  if (nameInput) nameInput.value = name || "";
678
692
  if (pathInput) pathInput.value = repo.path || "";
679
693
  if (testInput) testInput.value = repo.test || "";
680
694
  if (baseInput) baseInput.value = repo.base || "";
681
695
  if (titleInput) titleInput.value = repo.prTitle || "";
682
696
  if (branchInput) branchInput.value = repo.branch || "";
697
+ if (attemptsInput) attemptsInput.value = repo.maxAttempts != null && repo.maxAttempts !== "" ? String(repo.maxAttempts) : "";
698
+ if (roundsInput) roundsInput.value = repo.maxFixRounds != null && repo.maxFixRounds !== "" ? String(repo.maxFixRounds) : "";
683
699
  setPressed("#repo-deliver [data-deliver]", "data-deliver", repo.deliver === "pr" ? "pr" : "local");
684
700
  var browser = document.getElementById("repo-browser");
685
701
  if (browser) { browser.hidden = true; browser.innerHTML = ""; }
@@ -698,6 +714,8 @@
698
714
  base: value("repo-base"),
699
715
  prTitle: value("repo-pr-title"),
700
716
  branch: value("repo-branch"),
717
+ maxAttempts: value("repo-max-attempts"),
718
+ maxFixRounds: value("repo-max-fix-rounds"),
701
719
  };
702
720
  }
703
721
 
@@ -763,6 +781,13 @@
763
781
  renderRepoList();
764
782
  }
765
783
 
784
+ function optionalNumber(raw) {
785
+ var text = String(raw == null ? "" : raw).trim();
786
+ if (!text) return undefined;
787
+ if (!/^\d+(\.\d+)?$/.test(text)) return text;
788
+ return Number(text);
789
+ }
790
+
766
791
  function optionalInt(raw) {
767
792
  var text = String(raw == null ? "" : raw).trim();
768
793
  if (!text) return undefined;
@@ -791,6 +816,10 @@
791
816
  if (repo.base) entry.base = repo.base;
792
817
  if (repo.prTitle) entry.prTitle = repo.prTitle;
793
818
  if (repo.branch) entry.branch = repo.branch;
819
+ var repoAttempts = optionalInt(repo.maxAttempts);
820
+ var repoRounds = optionalInt(repo.maxFixRounds);
821
+ if (repoAttempts !== undefined) entry.maxAttempts = repoAttempts;
822
+ if (repoRounds !== undefined) entry.maxFixRounds = repoRounds;
794
823
  repos[name] = entry;
795
824
  }
796
825
  var body = { revision: settingsDoc ? settingsDoc.revision : "", repos: repos };
@@ -800,6 +829,10 @@
800
829
  if (attempts !== undefined) body.maxAttempts = attempts;
801
830
  if (stall !== undefined) body.stallTimeoutMs = stall;
802
831
  if (rounds !== undefined) body.maxFixRounds = rounds;
832
+ var fingerprint = optionalInt(inputValue("max-same-fingerprint"));
833
+ var cap = optionalNumber(inputValue("daily-dollar-cap"));
834
+ if (fingerprint !== undefined) body.maxSameFingerprint = fingerprint;
835
+ if (cap !== undefined) body.dailyDollarCap = cap;
803
836
  var mode = selectedAttr("#sandbox-mode [data-sandbox]", "data-sandbox", "off");
804
837
  var domains = domainList(inputValue("sandbox-domains"));
805
838
  var sandbox = {};
@@ -820,7 +853,8 @@
820
853
  if (repoField) {
821
854
  setSettingsTab("repos");
822
855
  if (Object.prototype.hasOwnProperty.call(repoDraft, repoField[1])) startEdit(repoField[1]);
823
- showSlot(repoField[2], message);
856
+ var slotName = repoField[2] === "maxAttempts" ? "repoAttempts" : repoField[2] === "maxFixRounds" ? "repoRounds" : repoField[2];
857
+ showSlot(slotName, message);
824
858
  continue;
825
859
  }
826
860
  if (key.indexOf("sandbox.allowDomains") === 0) {
@@ -828,7 +862,7 @@
828
862
  showSlot("sandbox.allowDomains", message);
829
863
  continue;
830
864
  }
831
- if (key === "sandbox.mode" || key === "maxAttempts" || key === "stallTimeoutMs" || key === "maxFixRounds") {
865
+ if (key === "sandbox.mode" || key === "maxAttempts" || key === "stallTimeoutMs" || key === "maxFixRounds" || key === "maxSameFingerprint" || key === "dailyDollarCap") {
832
866
  setSettingsTab("global");
833
867
  showSlot(key, message);
834
868
  continue;
@@ -977,8 +1011,9 @@
977
1011
  document.addEventListener("click", function (event) {
978
1012
  var target = event.target;
979
1013
  if (!target || !target.closest) return;
980
- if (target.closest("#open-settings")) {
981
- openSettings();
1014
+ var settingsControl = target.closest("#open-settings");
1015
+ if (settingsControl) {
1016
+ if (settingsControl.tagName !== "A") openSettings();
982
1017
  return;
983
1018
  }
984
1019
  if (target.closest("#add-job")) {
@@ -1135,7 +1170,7 @@
1135
1170
  return;
1136
1171
  }
1137
1172
  el.className = "status";
1138
- var jobs = document.querySelectorAll(".board .card").length;
1173
+ var jobs = document.querySelectorAll(".job").length;
1139
1174
  el.textContent = jobs + (jobs === 1 ? " job · " : " jobs · ") + new Date().toLocaleTimeString();
1140
1175
  localizeTimes(document);
1141
1176
  }
@@ -1154,6 +1189,27 @@
1154
1189
  location.reload();
1155
1190
  return;
1156
1191
  }
1192
+ var toggle = document.getElementById("pause-toggle");
1193
+ var form = document.getElementById("pause-form");
1194
+ if (toggle && form && data && typeof data.paused === "boolean") {
1195
+ toggle.textContent = data.paused ? "Resume" : "Pause";
1196
+ toggle.setAttribute("aria-pressed", data.paused ? "true" : "false");
1197
+ form.setAttribute("action", data.paused ? "/api/resume" : "/api/pause");
1198
+ var label = document.getElementById("run-label");
1199
+ var dot = document.getElementById("run-dot");
1200
+ if (label) label.textContent = data.paused ? "Paused" : "Running";
1201
+ if (dot) dot.classList.toggle("paused", data.paused);
1202
+ }
1203
+ var spend = document.getElementById("spend");
1204
+ if (spend && data && data.spend) {
1205
+ var capText = data.spend.cap == null ? "no cap" : "$" + Number(data.spend.cap).toFixed(2);
1206
+ spend.textContent = "today " + data.spend.day + " $" + Number(data.spend.usd).toFixed(2) + " / " + capText;
1207
+ var capNote = document.getElementById("cap-note");
1208
+ if (capNote) {
1209
+ var blocked = data.spend.blocked === true || (data.spend.cap != null && Number(data.spend.usd) >= Number(data.spend.cap));
1210
+ capNote.hidden = !blocked;
1211
+ }
1212
+ }
1157
1213
  stamp(true);
1158
1214
  })
1159
1215
  .catch(function () { stamp(false); });
@@ -1191,11 +1247,70 @@
1191
1247
  }
1192
1248
  var previewBtn = document.getElementById("preview-job");
1193
1249
  if (previewBtn) previewBtn.addEventListener("click", function () { submitIntake(true); });
1250
+ function readSeen() {
1251
+ try {
1252
+ var raw = localStorage.getItem("kiln-seen");
1253
+ var parsed = raw ? JSON.parse(raw) : {};
1254
+ return parsed && typeof parsed === "object" ? parsed : {};
1255
+ } catch (err) {
1256
+ return {};
1257
+ }
1258
+ }
1259
+
1260
+ function writeSeen(map) {
1261
+ try { localStorage.setItem("kiln-seen", JSON.stringify(map)); } catch (err) { /* private mode */ }
1262
+ }
1263
+
1264
+ function paintCount(node, count) {
1265
+ if (!node) return;
1266
+ if (!count) {
1267
+ node.hidden = true;
1268
+ node.textContent = "";
1269
+ return;
1270
+ }
1271
+ node.hidden = false;
1272
+ node.textContent = count === 1 ? "1 change since you last looked" : count + " changes since you last looked";
1273
+ }
1274
+
1275
+ function paintUnseen() {
1276
+ var seen = readSeen();
1277
+ var params = [];
1278
+ for (var id in seen) {
1279
+ if (!Object.prototype.hasOwnProperty.call(seen, id)) continue;
1280
+ if (typeof seen[id] !== "string" || !seen[id]) continue;
1281
+ params.push("seen=" + encodeURIComponent(id + "|" + seen[id]));
1282
+ }
1283
+ fetch("/api/unseen" + (params.length ? "?" + params.join("&") : ""), { cache: "no-store", headers: { Accept: "application/json" } })
1284
+ .then(function (response) { return response.ok ? response.json() : null; })
1285
+ .then(function (data) {
1286
+ if (!data) return;
1287
+ var counts = {};
1288
+ var jobs = data.jobs || [];
1289
+ for (var i = 0; i < jobs.length; i++) counts[jobs[i].jobId] = jobs[i].count;
1290
+ var nodes = document.querySelectorAll("[data-unseen-for]");
1291
+ for (var n = 0; n < nodes.length; n++) paintCount(nodes[n], counts[nodes[n].getAttribute("data-unseen-for")] || 0);
1292
+ paintCount(document.getElementById("inbox-unseen"), data.inbox || 0);
1293
+ })
1294
+ .catch(function () { /* the list still works without the badge */ });
1295
+ }
1296
+
1297
+ function markLooking() {
1298
+ var id = body.getAttribute("data-job");
1299
+ var latest = body.getAttribute("data-latest");
1300
+ if (!id || !latest) return;
1301
+ var seen = readSeen();
1302
+ seen[id] = latest;
1303
+ writeSeen(seen);
1304
+ }
1305
+
1194
1306
  showFresh();
1195
1307
  renderCompact();
1196
1308
  notifyLabel();
1197
1309
  applyRepo();
1198
1310
  stamp(true);
1311
+ paintUnseen();
1312
+ markLooking();
1313
+ if (document.getElementById("settings-form")) loadSettings();
1199
1314
  try {
1200
1315
  var open = localStorage.getItem("kiln-drawer");
1201
1316
  if (open) openJob(open);
package/dist/attention.js CHANGED
@@ -24,6 +24,8 @@ const RETRYABLE_REASONS = new Set(["stalled", "crashed", "orphaned", "failed"]);
24
24
  * so a waiting retry is not a call for a person.
25
25
  */
26
26
  export function scheduledRetry(job) {
27
+ if (job.waitReason)
28
+ return false;
27
29
  if (job.state !== "failed")
28
30
  return false;
29
31
  if (!job.nextRetryAt || !Number.isFinite(Date.parse(job.nextRetryAt)))
@@ -92,13 +94,18 @@ function toItem(job) {
92
94
  reason: attentionReason(job, tail),
93
95
  ...(tail ? { errorTail: tail } : {}),
94
96
  ...(findings ? { findings } : {}),
95
- retry: job.state === "failed" || job.state === "dead",
97
+ retry: (job.state === "failed" || job.state === "dead" || (job.state === "needs-human" && Boolean(job.stopReason))) &&
98
+ job.gates.length === 0,
96
99
  ...(since ? { since } : {}),
97
100
  };
98
101
  }
99
102
  export function attentionReason(job, tail = errorTail(job)) {
100
103
  switch (job.state) {
101
104
  case "needs-human": {
105
+ if (job.stopReason === "attempt-cap")
106
+ return "Attempt cap reached";
107
+ if (job.stopReason === "fingerprint")
108
+ return "Same failure repeated";
102
109
  if (job.feedbackReason)
103
110
  return job.feedbackReason;
104
111
  const question = gateQuestion(job);
@@ -109,6 +116,8 @@ export function attentionReason(job, tail = errorTail(job)) {
109
116
  return AUTH_FAILED_LINE;
110
117
  return job.sandboxError ? `Sandbox: ${oneLine(job.sandboxError)}` : "Run died before it finished";
111
118
  case "failed": {
119
+ if (job.waitReason)
120
+ return job.waitReason;
112
121
  if (job.lastFailureReason === "auth-failed")
113
122
  return AUTH_FAILED_LINE;
114
123
  if (job.sandboxError)
@@ -124,8 +133,6 @@ export function attentionReason(job, tail = errorTail(job)) {
124
133
  return "In review";
125
134
  case "invalid":
126
135
  return job.invalidReason ? `Invalid: ${oneLine(job.invalidReason)}` : "Job file is invalid";
127
- case "blocked":
128
- return oneLine(waitingOn(job));
129
136
  case "running": {
130
137
  const current = job.steps.find((step) => step.phase === "current" || step.phase === "failed");
131
138
  return current ? `Running ${current.id}` : "Running";
@@ -133,7 +140,9 @@ export function attentionReason(job, tail = errorTail(job)) {
133
140
  case "approved":
134
141
  return "Approved, still finishing";
135
142
  case "queued":
136
- return "Queued, ready to launch";
143
+ return job.waitReason ?? "Queued, ready to launch";
144
+ case "blocked":
145
+ return job.waitReason ?? oneLine(waitingOn(job));
137
146
  case "done":
138
147
  return job.prUrl ? "Merged" : "Done";
139
148
  case "rejected":