@lchase/kiln 0.8.0 → 0.10.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/README.md +6 -4
- package/assets/serve.css +13 -0
- package/assets/serve.js +49 -3
- package/dist/attention.js +13 -4
- package/dist/cli.js +112 -8
- package/dist/config.js +25 -1
- package/dist/delivery.js +18 -0
- package/dist/driver.js +213 -14
- package/dist/events.js +393 -0
- package/dist/evidence.js +140 -0
- package/dist/failure.js +140 -0
- package/dist/fix-commit.js +73 -0
- package/dist/jobs.js +12 -0
- package/dist/ledger.js +152 -0
- package/dist/project.js +394 -0
- package/dist/rails.js +164 -0
- package/dist/reconcile.js +29 -3
- package/dist/redact.js +63 -0
- package/dist/reliability.js +75 -26
- package/dist/report.js +19 -5
- package/dist/schemas.js +35 -5
- package/dist/serve-page.js +21 -1
- package/dist/serve.js +86 -4
- package/dist/settings.js +9 -1
- package/package.json +2 -2
- package/workflows/fix.yaml +1 -1
package/README.md
CHANGED
|
@@ -108,7 +108,7 @@ Then set the kiln config to `"sandbox": { "mode": "docker" }`.
|
|
|
108
108
|
|
|
109
109
|
`kiln tick` and `kiln serve` name what is missing when sandbox mode is on: the docker binary, the docker daemon, each image, and the credential. They also refuse an installed `@lchase/yak` that does not pass `allowedDomains` from the agent step into the sandbox proxy. `@lchase/yak` 0.8.0 does. An older package drops `sandbox`, `image`, and `allowedDomains` while loading YAML and opens the proxy with only `api.anthropic.com`, so the job would run on the host with the open internet. Kiln leaves that job failed until `kiln retry`, after the installed package is 0.8.0 or newer.
|
|
110
110
|
|
|
111
|
-
A
|
|
111
|
+
A sandbox or container start flake (the container never started, the daemon was unreachable) is transient and retries with the usual backoff. Any other yak `sandbox-error` is not retried. The card shows the failure detail, and a sandboxed job shows `sandbox · docker`.
|
|
112
112
|
|
|
113
113
|
## Register repos
|
|
114
114
|
|
|
@@ -271,12 +271,14 @@ while true; do npm run kiln -- tick; sleep 30; done
|
|
|
271
271
|
|
|
272
272
|
A job is ready when its dependencies are met and the number of jobs in flight is under `maxConcurrent`. The default is after merged: every job in `dependsOn` is `done`. `--stack` is met when that one upstream has landed. A rejected gate does not unblock the next job. An ok run with no readable approval does not either. An authentication failure does not either: reason `auth-failed` stays in Needs human until `kiln retry`.
|
|
273
273
|
|
|
274
|
-
`stallTimeoutMs` (default `900000`, 15 minutes), `maxAttempts` (default `3`), and `maxFixRounds` (default `2`) live in the kiln config. `stallTimeoutMs` of `0` or below turns stall detection off. A running job with no yak journal event and no log output for that long is killed, process tree included, and marked failed with reason `stalled`. A process that is already gone when `kiln tick` or `kiln serve` starts is `orphaned`. An agent or launch crash is `crashed`. A non-zero exit that is not a gate rejection is `failed`. Those four retry on their own after `10s × 2^(n-1)`, capped at 5 minutes. A rejected gate, a cancelled run, and an ok run with no readable approval do not. After `maxAttempts` the job is `
|
|
274
|
+
`stallTimeoutMs` (default `900000`, 15 minutes), `maxAttempts` (default `3`), and `maxFixRounds` (default `2`) live in the kiln config. `stallTimeoutMs` of `0` or below turns stall detection off. A running job with no yak journal event and no log output for that long is killed, process tree included, and marked failed with reason `stalled`. A process that is already gone when `kiln tick` or `kiln serve` starts is `orphaned`. An agent or launch crash is `crashed`. A non-zero exit that is not a gate rejection is `failed`. Those four retry on their own after `10s × 2^(n-1)`, capped at 5 minutes. A rejected gate, a cancelled run, and an ok run with no readable approval do not. After `maxAttempts` the failure is class `budget` and the job is `needs-human`. The same fingerprint does that after `maxSameFingerprint` (default 3) even sooner. A test, build, or type error is class `deterministic` and is not retried. `repos.<name>.maxAttempts` and `repos.<name>.maxFixRounds` override the global caps. `dailyDollarCap`, when set, blocks new runs once today's recorded dollars reach it. `kiln pause` does the same until `kiln resume`. In-flight runs finish. `kiln revise <id> --note <text>` sends a gated job around again with that note in the next prompt. Each attempt is a file under `<kiln-dir>/attempts/<job-id>.json`, so state stays derived. Status prints `attempts`, `reason`, and `next retry` from the same view `readStatus` returns. On the board, a dead card in Needs human shows `attempt N/M` and the reason. A failure that still has a scheduled retry stays in its stage column instead, with `retrying` and the local time.
|
|
275
275
|
|
|
276
276
|
While a `deliver=pr` job is `in-review`, tick reads that pull request with `gh`: check runs for the head, reviews, and unresolved review comments. The operator's inline comments and unresolved threads count; a top-level note counts only when it starts with `/kiln`, and that prefix is left out of the fix prompt. Comments from `kiln` or a `[bot]` login are ignored, and a resolved thread does not start a round. Pending checks are ignored. A completed failing check on a head that no round has recorded, or review feedback newer than the last round, starts `workflows/fix.yaml` with `yak run --from` that head. The prompt is the original spec plus the failing check names (with a short log excerpt) and the review comment bodies, including file and line. The agent commits on `yak/<run-id>` and does not push. Kiln fast-forwards the open pull request branch. It does not open a second pull request, force-push, merge, approve, or comment. The same head and the same feedback do not start two runs. Each round is a file under `<kiln-dir>/feedback/<job-id>.json`. A stall or crash during a fix uses the attempt ledger above. After `maxFixRounds` the job is `needs-human` with reason `feedback-exhausted`. Feedback Kiln cannot classify is `needs-human` with reason `feedback-ambiguous`. The person merges the pull request.
|
|
277
277
|
|
|
278
278
|
`npm run kiln -- status` lists jobs across repos. When a job has `source`, status prints `source <kind> <ref>`. For `needs-human` it prints `approve: kiln approve <id>` and `reject: kiln reject <id>`. Run those as `npm run kiln -- approve <id>` and `npm run kiln -- reject <id>`. For `done` and `in-review` it prints the branch. `deliver=local` prints `branch yak/<run-id>`. `deliver=pr` prints the remote branch name recorded on the delivery, and the pull request url once one exists. `auth-failed` prints `Claude login rejected (401) — check CLAUDE_CODE_OAUTH_TOKEN / claude login, then Retry`. For an investigation whose findings file exists, it prints `findings <path>`. That path is `findingsPath` on the same status view `kiln serve` renders, and the console links it. For `blocked` on a dependency id that has no job file, it prints `blocked on missing job <id>`. While an upstream pull request is open, it prints `waiting for PR #N to merge`. `npm run kiln -- retry <id>` relaunches a failed or dead job, or delivers again when the only failure is opening the pull request, and clears the attempt ledger.
|
|
279
279
|
|
|
280
|
+
`npm run kiln -- ledger` prints today's recorded dollars against `dailyDollarCap` (or `no cap`) and counts human interventions from `<kiln-home>/events.jsonl`: a rejected gate, a job retried by hand, an edit noted on a gate answer, and a needs-human stop. `--since 7d` (also `12h`, `30m`, `45s`, or an ISO time) keeps events at or after that instant. `--repo <name>` limits the count to one repo. A failed append is reported and does not fail the job. `GET /api/events?job_id=` on `kiln serve` returns that job's lines. A pull request body gains a short Evidence section from the same lines, with tokens and secret env values masked.
|
|
281
|
+
|
|
280
282
|
State is derived from the yak journal, the gate files, and (for `deliver=pr`) `gh pr view`:
|
|
281
283
|
|
|
282
284
|
| State | Meaning |
|
|
@@ -284,11 +286,11 @@ State is derived from the yak journal, the gate files, and (for `deliver=pr`) `g
|
|
|
284
286
|
| `queued` | Dependencies are met, and the job has not started |
|
|
285
287
|
| `blocked` | An after-merged dependency is not `done`, or a stacked dependency has not landed. A missing job id is named |
|
|
286
288
|
| `running` | Yak is alive, or the launch process is alive and has not written a journal |
|
|
287
|
-
| `needs-human` |
|
|
289
|
+
| `needs-human` | A gate is open, feedback is exhausted or ambiguous, the attempt cap is used, or the same fingerprint has failed `maxSameFingerprint` times. `kiln retry` clears an attempt-cap or fingerprint stop. `kiln revise --note` sends a gate back with a note |
|
|
288
290
|
| `approved` | The gate says approve and the run has not finished. This still holds a slot |
|
|
289
291
|
| `rejected` | The run finished ok and the gate says reject |
|
|
290
292
|
| `failed` | The run failed, the launch died before a journal, the run finished ok with no readable approval, a pull request could not be opened, a Claude login was rejected (`auth-failed`, not retried), or a transient failure is waiting to retry |
|
|
291
|
-
| `dead` |
|
|
293
|
+
| `dead` | A journal whose process is already gone, before the next tick records it. Status prints the cancel command when a process is gone |
|
|
292
294
|
| `in-review` | `deliver=pr` and the run has landed. The pull request is open, or the next tick still has to open it |
|
|
293
295
|
| `done` | `deliver=local`: the run finished ok and the gate says approve. `deliver=pr`: that pull request has merged. An investigation is `done` when the gate says approve, including when the repo is `deliver=pr`. It does not enter `in-review` |
|
|
294
296
|
|
package/assets/serve.css
CHANGED
|
@@ -61,6 +61,19 @@ header {
|
|
|
61
61
|
}
|
|
62
62
|
.ver { font-size: 12px; line-height: 16px; color: var(--faint); align-self: flex-end; margin: 0 0 4px -4px; }
|
|
63
63
|
h1 { margin: 0; font-size: 18px; line-height: 24px; font-weight: 500; }
|
|
64
|
+
.spend { font-family: var(--mono); font-size: 12px; line-height: 16px; color: var(--muted); }
|
|
65
|
+
.pause { margin: 0; }
|
|
66
|
+
.pause button, .d-actions .revise input {
|
|
67
|
+
font: inherit;
|
|
68
|
+
color: var(--ink);
|
|
69
|
+
background: var(--card);
|
|
70
|
+
border: 1px solid var(--line-strong);
|
|
71
|
+
border-radius: 6px;
|
|
72
|
+
padding: 6px 10px;
|
|
73
|
+
}
|
|
74
|
+
.pause button[aria-pressed="true"] { color: var(--human); }
|
|
75
|
+
.d-actions .revise { display: flex; gap: 8px; align-items: center; }
|
|
76
|
+
.d-actions .revise input { min-width: 16rem; }
|
|
64
77
|
.status { margin-left: auto; font-family: var(--mono); font-size: 12px; line-height: 16px; color: var(--muted); }
|
|
65
78
|
.status::before {
|
|
66
79
|
content: "";
|
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
|
|
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
|
-
|
|
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;
|
|
@@ -1154,6 +1188,18 @@
|
|
|
1154
1188
|
location.reload();
|
|
1155
1189
|
return;
|
|
1156
1190
|
}
|
|
1191
|
+
var toggle = document.getElementById("pause-toggle");
|
|
1192
|
+
var form = document.getElementById("pause-form");
|
|
1193
|
+
if (toggle && form && data && typeof data.paused === "boolean") {
|
|
1194
|
+
toggle.textContent = data.paused ? "Resume" : "Pause";
|
|
1195
|
+
toggle.setAttribute("aria-pressed", data.paused ? "true" : "false");
|
|
1196
|
+
form.setAttribute("action", data.paused ? "/api/resume" : "/api/pause");
|
|
1197
|
+
}
|
|
1198
|
+
var spend = document.getElementById("spend");
|
|
1199
|
+
if (spend && data && data.spend) {
|
|
1200
|
+
var capText = data.spend.cap == null ? "no cap" : "$" + Number(data.spend.cap).toFixed(2);
|
|
1201
|
+
spend.textContent = "today " + data.spend.day + " $" + Number(data.spend.usd).toFixed(2) + " / " + capText;
|
|
1202
|
+
}
|
|
1157
1203
|
stamp(true);
|
|
1158
1204
|
})
|
|
1159
1205
|
.catch(function () { stamp(false); });
|
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":
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { addRepo, renderRepoList, resolveKilnConfig } from "./config.js";
|
|
3
3
|
import { renderSandboxStatus, runSandboxBuild } from "./sandbox.js";
|
|
4
|
-
import { answerJob, retryJob, runStatus, runTick } from "./driver.js";
|
|
4
|
+
import { answerJob, retryJob, reviseJob, runStatus, runTick } from "./driver.js";
|
|
5
|
+
import { pauseFactory, resumeFactory } from "./rails.js";
|
|
6
|
+
import { parseSince, renderLedgerFile } from "./ledger.js";
|
|
5
7
|
import { startServe } from "./serve.js";
|
|
6
8
|
import { addFromFile } from "./intake/file.js";
|
|
7
9
|
import { addFromJira } from "./intake/jira.js";
|
|
@@ -18,7 +20,11 @@ function usage() {
|
|
|
18
20
|
" kiln status [jobId] [--config <file>]",
|
|
19
21
|
" kiln approve <jobId> [--config <file>]",
|
|
20
22
|
" kiln reject <jobId> [--config <file>]",
|
|
23
|
+
" kiln revise <jobId> --note <text> [--config <file>]",
|
|
21
24
|
" kiln retry <jobId> [--config <file>]",
|
|
25
|
+
" kiln pause [--config <file>]",
|
|
26
|
+
" kiln resume [--config <file>]",
|
|
27
|
+
" kiln ledger [--since 7d] [--repo <name>] [--config <file>]",
|
|
22
28
|
" kiln sandbox [build] [--config <file>]",
|
|
23
29
|
" kiln serve [--port 4100] [--tick <seconds>|--no-tick] [--config <file>]",
|
|
24
30
|
"",
|
|
@@ -27,7 +33,7 @@ function usage() {
|
|
|
27
33
|
"A relative path is stored absolute, resolved from the working directory.",
|
|
28
34
|
"~ is expanded from the home directory.",
|
|
29
35
|
"",
|
|
30
|
-
"Config, jobs, logs, and deliveries live in one kiln home: --config <file>",
|
|
36
|
+
"Config, jobs, logs, events, and deliveries live in one kiln home: --config <file>",
|
|
31
37
|
"if given, otherwise $KILN_HOME/kiln.config.json when KILN_HOME is set,",
|
|
32
38
|
"otherwise ~/.kiln/kiln.config.json. repo add creates that directory.",
|
|
33
39
|
"Run kiln repo add <name> <path> from any directory. A leftover",
|
|
@@ -64,8 +70,20 @@ function usage() {
|
|
|
64
70
|
"or a process that is already gone when tick or serve starts (orphaned) retries",
|
|
65
71
|
"after 10s, 20s, 40s, and so on, capped at 5 minutes, up to maxAttempts",
|
|
66
72
|
"A rejected Claude login (401 or an invalid bearer token) is auth-failed and is not retried.",
|
|
67
|
-
"(default 3).
|
|
73
|
+
"(default 3, overridable per repo). Hitting the cap is a budget failure and the",
|
|
74
|
+
"job needs a person. The same failure fingerprint stops the job after",
|
|
75
|
+
"maxSameFingerprint (default 3). A test, build, or type error is not retried.",
|
|
76
|
+
"A rejected gate is not retried. revise sends the job around again with the",
|
|
77
|
+
"note in the next prompt. pause stops new runs until resume. In-flight runs",
|
|
78
|
+
"finish. A daily dollar cap, when set, does the same once today's recorded",
|
|
79
|
+
"spend reaches it. ledger prints that spend.",
|
|
68
80
|
"Attempts are files under the kiln home, not fields on the job.",
|
|
81
|
+
"Events are one JSON line each in <kiln-home>/events.jsonl. A write",
|
|
82
|
+
"failure is reported and does not stop the job. kiln ledger counts",
|
|
83
|
+
"human interventions from that log: a rejected gate, a manual retry,",
|
|
84
|
+
"an edit noted on a gate, and a needs-human stop. --since 7d (or 12h,",
|
|
85
|
+
"30m, 45s, or an ISO time) keeps events at or after that time. --repo",
|
|
86
|
+
"limits the count to one registered repo name.",
|
|
69
87
|
"Once a pull request is open, tick watches it. A failed check or a review",
|
|
70
88
|
"that requests changes starts a fix on that same branch, up to maxFixRounds",
|
|
71
89
|
"(default 2). Kiln does not merge, approve, or comment. Past that bound the",
|
|
@@ -219,6 +237,20 @@ function parseFlags(argv) {
|
|
|
219
237
|
bag.noTick = true;
|
|
220
238
|
continue;
|
|
221
239
|
}
|
|
240
|
+
if (flag === "--note") {
|
|
241
|
+
if (!next || next.startsWith("--"))
|
|
242
|
+
usage();
|
|
243
|
+
bag.note = next;
|
|
244
|
+
i += 1;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (flag === "--since") {
|
|
248
|
+
if (!next || next.startsWith("--"))
|
|
249
|
+
usage();
|
|
250
|
+
bag.since = next;
|
|
251
|
+
i += 1;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
222
254
|
if (flag?.startsWith("--"))
|
|
223
255
|
usage();
|
|
224
256
|
if (flag !== undefined)
|
|
@@ -226,9 +258,11 @@ function parseFlags(argv) {
|
|
|
226
258
|
}
|
|
227
259
|
return bag;
|
|
228
260
|
}
|
|
229
|
-
function rejectServeFlags(flags) {
|
|
261
|
+
function rejectServeFlags(flags, options = {}) {
|
|
230
262
|
if (flags.port !== undefined || flags.tickSeconds !== undefined || flags.noTick)
|
|
231
263
|
usage();
|
|
264
|
+
if (options.since !== false && flags.since)
|
|
265
|
+
usage();
|
|
232
266
|
}
|
|
233
267
|
function repoCommand(argv) {
|
|
234
268
|
const sub = argv[0];
|
|
@@ -363,7 +397,7 @@ function sandboxCommand(argv) {
|
|
|
363
397
|
function tickCommand(argv) {
|
|
364
398
|
const flags = parseFlags(argv);
|
|
365
399
|
rejectServeFlags(flags);
|
|
366
|
-
if (flags.positionals.length !== 0 || flags.test || hasIntakeFlags(flags) || flags.deliver || flags.base || flags.stack) {
|
|
400
|
+
if (flags.positionals.length !== 0 || flags.test || flags.note || hasIntakeFlags(flags) || flags.deliver || flags.base || flags.stack) {
|
|
367
401
|
usage();
|
|
368
402
|
}
|
|
369
403
|
const config = resolveKilnConfig({ configPath: flags.configPath });
|
|
@@ -372,7 +406,7 @@ function tickCommand(argv) {
|
|
|
372
406
|
function statusCommand(argv) {
|
|
373
407
|
const flags = parseFlags(argv);
|
|
374
408
|
rejectServeFlags(flags);
|
|
375
|
-
if (flags.positionals.length > 1 || flags.test || hasIntakeFlags(flags) || flags.deliver || flags.base || flags.stack) {
|
|
409
|
+
if (flags.positionals.length > 1 || flags.test || flags.note || hasIntakeFlags(flags) || flags.deliver || flags.base || flags.stack) {
|
|
376
410
|
usage();
|
|
377
411
|
}
|
|
378
412
|
const config = resolveKilnConfig({ configPath: flags.configPath });
|
|
@@ -382,17 +416,71 @@ function retryCommand(argv) {
|
|
|
382
416
|
const flags = parseFlags(argv);
|
|
383
417
|
rejectServeFlags(flags);
|
|
384
418
|
const jobId = flags.positionals[0];
|
|
385
|
-
if (!jobId || flags.positionals.length !== 1 || flags.test || hasIntakeFlags(flags) || flags.deliver || flags.base || flags.stack) {
|
|
419
|
+
if (!jobId || flags.positionals.length !== 1 || flags.test || flags.note || hasIntakeFlags(flags) || flags.deliver || flags.base || flags.stack) {
|
|
386
420
|
usage();
|
|
387
421
|
}
|
|
388
422
|
const config = resolveKilnConfig({ configPath: flags.configPath });
|
|
389
423
|
process.stdout.write(retryJob(config, jobId));
|
|
390
424
|
}
|
|
425
|
+
function ledgerCommand(argv) {
|
|
426
|
+
const flags = parseFlags(argv);
|
|
427
|
+
rejectServeFlags(flags, { since: false });
|
|
428
|
+
if (flags.positionals.length !== 0 ||
|
|
429
|
+
flags.test ||
|
|
430
|
+
flags.spec ||
|
|
431
|
+
flags.prompt ||
|
|
432
|
+
flags.jira ||
|
|
433
|
+
flags.title ||
|
|
434
|
+
flags.issue ||
|
|
435
|
+
flags.workflow ||
|
|
436
|
+
flags.context ||
|
|
437
|
+
flags.dryRun ||
|
|
438
|
+
flags.stack ||
|
|
439
|
+
flags.after.length > 0 ||
|
|
440
|
+
flags.deliver ||
|
|
441
|
+
flags.base) {
|
|
442
|
+
usage();
|
|
443
|
+
}
|
|
444
|
+
const config = resolveKilnConfig({ configPath: flags.configPath });
|
|
445
|
+
const since = flags.since ? parseSince(flags.since) : undefined;
|
|
446
|
+
process.stdout.write(renderLedgerFile(config.kilnDir, {
|
|
447
|
+
...(since ? { since } : {}),
|
|
448
|
+
...(flags.repo ? { repo: flags.repo } : {}),
|
|
449
|
+
...(config.dailyDollarCap !== undefined ? { dailyDollarCap: config.dailyDollarCap } : {}),
|
|
450
|
+
}));
|
|
451
|
+
}
|
|
452
|
+
function reviseCommand(argv) {
|
|
453
|
+
const flags = parseFlags(argv);
|
|
454
|
+
rejectServeFlags(flags);
|
|
455
|
+
const jobId = flags.positionals[0];
|
|
456
|
+
if (!jobId ||
|
|
457
|
+
flags.positionals.length !== 1 ||
|
|
458
|
+
!flags.note ||
|
|
459
|
+
flags.test ||
|
|
460
|
+
hasIntakeFlags(flags) ||
|
|
461
|
+
flags.deliver ||
|
|
462
|
+
flags.base ||
|
|
463
|
+
flags.stack) {
|
|
464
|
+
usage();
|
|
465
|
+
}
|
|
466
|
+
const config = resolveKilnConfig({ configPath: flags.configPath });
|
|
467
|
+
process.stdout.write(reviseJob(config, jobId, flags.note));
|
|
468
|
+
}
|
|
469
|
+
function pauseCommand(argv, pause) {
|
|
470
|
+
const flags = parseFlags(argv);
|
|
471
|
+
rejectServeFlags(flags);
|
|
472
|
+
if (flags.positionals.length !== 0 || flags.test || hasIntakeFlags(flags) || flags.note || flags.deliver || flags.base || flags.stack) {
|
|
473
|
+
usage();
|
|
474
|
+
}
|
|
475
|
+
const config = resolveKilnConfig({ configPath: flags.configPath });
|
|
476
|
+
const changed = pause ? pauseFactory(config.kilnDir) : resumeFactory(config.kilnDir);
|
|
477
|
+
process.stdout.write(pause ? (changed ? "paused\n" : "already paused\n") : changed ? "resumed\n" : "already running\n");
|
|
478
|
+
}
|
|
391
479
|
function gateCommand(choice, argv) {
|
|
392
480
|
const flags = parseFlags(argv);
|
|
393
481
|
rejectServeFlags(flags);
|
|
394
482
|
const jobId = flags.positionals[0];
|
|
395
|
-
if (!jobId || flags.positionals.length !== 1 || flags.test || hasIntakeFlags(flags) || flags.deliver || flags.base || flags.stack) {
|
|
483
|
+
if (!jobId || flags.positionals.length !== 1 || flags.test || flags.note || hasIntakeFlags(flags) || flags.deliver || flags.base || flags.stack) {
|
|
396
484
|
usage();
|
|
397
485
|
}
|
|
398
486
|
const config = resolveKilnConfig({ configPath: flags.configPath });
|
|
@@ -468,6 +556,22 @@ async function main() {
|
|
|
468
556
|
retryCommand(argv.slice(1));
|
|
469
557
|
return;
|
|
470
558
|
}
|
|
559
|
+
if (command === "revise") {
|
|
560
|
+
reviseCommand(argv.slice(1));
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
if (command === "pause") {
|
|
564
|
+
pauseCommand(argv.slice(1), true);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (command === "resume") {
|
|
568
|
+
pauseCommand(argv.slice(1), false);
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
if (command === "ledger") {
|
|
572
|
+
ledgerCommand(argv.slice(1));
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
471
575
|
if (command === "sandbox") {
|
|
472
576
|
sandboxCommand(argv.slice(1));
|
|
473
577
|
return;
|
package/dist/config.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "no
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
4
|
import { KilnConfigError } from "./errors.js";
|
|
5
|
-
import { DEFAULT_MAX_ATTEMPTS, DEFAULT_MAX_FIX_ROUNDS, DEFAULT_STALL_TIMEOUT_MS, KilnConfigFileSchema, REPO_NAME, } from "./schemas.js";
|
|
5
|
+
import { DEFAULT_MAX_ATTEMPTS, DEFAULT_MAX_FIX_ROUNDS, DEFAULT_MAX_SAME_FINGERPRINT, DEFAULT_STALL_TIMEOUT_MS, KilnConfigFileSchema, REPO_NAME, } from "./schemas.js";
|
|
6
6
|
const CONFIG_NAME = "kiln.config.json";
|
|
7
7
|
export function loadKilnConfig(configPath, options = {}) {
|
|
8
8
|
const home = options.home ?? homedir();
|
|
@@ -31,6 +31,8 @@ export function loadKilnConfig(configPath, options = {}) {
|
|
|
31
31
|
...(entry.base ? { base: entry.base } : {}),
|
|
32
32
|
...(entry.prTitle ? { prTitle: entry.prTitle } : {}),
|
|
33
33
|
...(entry.branch ? { branchTemplate: entry.branch } : {}),
|
|
34
|
+
...(entry.maxAttempts !== undefined ? { maxAttempts: entry.maxAttempts } : {}),
|
|
35
|
+
...(entry.maxFixRounds !== undefined ? { maxFixRounds: entry.maxFixRounds } : {}),
|
|
34
36
|
};
|
|
35
37
|
}
|
|
36
38
|
const kilnDir = configDir;
|
|
@@ -46,6 +48,8 @@ export function loadKilnConfig(configPath, options = {}) {
|
|
|
46
48
|
stallTimeoutMs: parsed.data.stallTimeoutMs ?? DEFAULT_STALL_TIMEOUT_MS,
|
|
47
49
|
maxAttempts: parsed.data.maxAttempts ?? DEFAULT_MAX_ATTEMPTS,
|
|
48
50
|
maxFixRounds: parsed.data.maxFixRounds ?? DEFAULT_MAX_FIX_ROUNDS,
|
|
51
|
+
maxSameFingerprint: parsed.data.maxSameFingerprint ?? DEFAULT_MAX_SAME_FINGERPRINT,
|
|
52
|
+
...(parsed.data.dailyDollarCap !== undefined ? { dailyDollarCap: parsed.data.dailyDollarCap } : {}),
|
|
49
53
|
adapter: parsed.data.adapter ?? "claude-code",
|
|
50
54
|
sandbox: parsed.data.sandbox ?? {},
|
|
51
55
|
workflowSandbox: workflowSandboxFrom(parsed.data.workflows),
|
|
@@ -112,6 +116,12 @@ export function addRepo(options) {
|
|
|
112
116
|
const branchTemplate = previousString(previous, "branch");
|
|
113
117
|
if (branchTemplate)
|
|
114
118
|
entry.branch = branchTemplate;
|
|
119
|
+
const maxAttempts = previousNumber(previous, "maxAttempts");
|
|
120
|
+
if (maxAttempts !== undefined)
|
|
121
|
+
entry.maxAttempts = maxAttempts;
|
|
122
|
+
const maxFixRounds = previousNumber(previous, "maxFixRounds");
|
|
123
|
+
if (maxFixRounds !== undefined)
|
|
124
|
+
entry.maxFixRounds = maxFixRounds;
|
|
115
125
|
repos[options.name] = entry;
|
|
116
126
|
raw.repos = repos;
|
|
117
127
|
const parsed = KilnConfigFileSchema.safeParse(raw);
|
|
@@ -138,6 +148,14 @@ export function addRepo(options) {
|
|
|
138
148
|
lines.push(`wrote ${configPath}`);
|
|
139
149
|
return `${lines.join("\n")}\n`;
|
|
140
150
|
}
|
|
151
|
+
/** Attempt cap for one repo: the repo override, otherwise the global cap. */
|
|
152
|
+
export function attemptsFor(config, repo) {
|
|
153
|
+
return config.repos[repo]?.maxAttempts ?? config.maxAttempts;
|
|
154
|
+
}
|
|
155
|
+
/** Fix-round cap for one repo: the repo override, otherwise the global cap. */
|
|
156
|
+
export function fixRoundsFor(config, repo) {
|
|
157
|
+
return config.repos[repo]?.maxFixRounds ?? config.maxFixRounds;
|
|
158
|
+
}
|
|
141
159
|
export function renderRepoList(config) {
|
|
142
160
|
const names = Object.keys(config.repos).sort();
|
|
143
161
|
if (names.length === 0)
|
|
@@ -226,6 +244,12 @@ function previousString(previous, key) {
|
|
|
226
244
|
const value = previous[key];
|
|
227
245
|
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
228
246
|
}
|
|
247
|
+
function previousNumber(previous, key) {
|
|
248
|
+
if (typeof previous !== "object" || previous === null || !(key in previous))
|
|
249
|
+
return undefined;
|
|
250
|
+
const value = previous[key];
|
|
251
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
252
|
+
}
|
|
229
253
|
function previousDeliver(previous) {
|
|
230
254
|
const value = previousString(previous, "deliver");
|
|
231
255
|
if (value === "local" || value === "pr")
|
package/dist/delivery.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import { evidenceSectionFor } from "./evidence.js";
|
|
5
|
+
import { appendEvent } from "./events.js";
|
|
4
6
|
import { HostError } from "./host.js";
|
|
5
7
|
import { readJobSpec } from "./jobs.js";
|
|
6
8
|
import { jobBasename } from "./paths.js";
|
|
@@ -181,6 +183,20 @@ export function deliverJob(request) {
|
|
|
181
183
|
prNumber: pr.number,
|
|
182
184
|
prUrl: pr.url,
|
|
183
185
|
});
|
|
186
|
+
if (!sameRun?.prNumber && remoteName) {
|
|
187
|
+
appendEvent(request.config.kilnDir, {
|
|
188
|
+
job_id: request.job.id,
|
|
189
|
+
run_id: request.runId,
|
|
190
|
+
event: "pr.opened",
|
|
191
|
+
properties: {
|
|
192
|
+
number: pr.number,
|
|
193
|
+
url: pr.url,
|
|
194
|
+
branch: remoteName,
|
|
195
|
+
base: request.baseBranch,
|
|
196
|
+
once: `pr.opened:${pr.number}`,
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
}
|
|
184
200
|
return { ok: true, detail: pr.url, prUrl: pr.url, prNumber: pr.number };
|
|
185
201
|
}
|
|
186
202
|
catch (error) {
|
|
@@ -213,6 +229,8 @@ export function pullRequestBody(request) {
|
|
|
213
229
|
"## Reviewers",
|
|
214
230
|
...(reviews.length > 0 ? reviews : ["- (no reviewer artifacts)"]),
|
|
215
231
|
"",
|
|
232
|
+
evidenceSectionFor(request.config.kilnDir, request.job.id).trimEnd(),
|
|
233
|
+
"",
|
|
216
234
|
].join("\n");
|
|
217
235
|
}
|
|
218
236
|
export function specSummary(specText) {
|