@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/README.md +5 -5
- package/assets/serve.css +195 -472
- package/assets/serve.js +121 -6
- package/dist/attention.js +13 -4
- package/dist/board-view.js +688 -0
- package/dist/board.js +0 -97
- package/dist/cli.js +63 -6
- package/dist/config.js +25 -1
- package/dist/driver.js +117 -16
- package/dist/events.js +40 -1
- package/dist/failure.js +140 -0
- package/dist/fix-commit.js +73 -0
- package/dist/ledger.js +11 -3
- package/dist/project.js +20 -4
- package/dist/rails.js +164 -0
- package/dist/reconcile.js +29 -3
- package/dist/reliability.js +53 -25
- package/dist/report.js +19 -5
- package/dist/schemas.js +35 -5
- package/dist/serve-page.js +396 -316
- package/dist/serve.js +252 -13
- package/dist/settings.js +9 -1
- package/package.json +4 -3
- package/workflows/fix.yaml +1 -1
package/dist/serve.js
CHANGED
|
@@ -5,15 +5,17 @@ import { homedir } from "node:os";
|
|
|
5
5
|
import { join, resolve, sep } from "node:path";
|
|
6
6
|
import { AUTH_PREFLIGHT_PREFIX } from "./auth.js";
|
|
7
7
|
import { loadKilnConfig } from "./config.js";
|
|
8
|
-
import { answerJob, readStatus, retryJob, runTick, startupRecovery } from "./driver.js";
|
|
8
|
+
import { answerJob, readStatus, retryJob, reviseJob, runTick, startupRecovery } from "./driver.js";
|
|
9
9
|
import { browse } from "./fs-browse.js";
|
|
10
|
-
import {
|
|
10
|
+
import { branchOf, buildBoard, dayReport, jobDetail, parseSeenQuery, parseStatsWindow, readBranchCommits, readDiffStat, statsFor, unseenChanges, } from "./board-view.js";
|
|
11
|
+
import { appendEvent, eventsForJob, readEvents } from "./events.js";
|
|
11
12
|
import { INVESTIGATION_WORKFLOW } from "./findings.js";
|
|
12
13
|
import { defaultHost } from "./host.js";
|
|
13
14
|
import { intakeJobs } from "./intake-http.js";
|
|
14
|
-
import { packageRoot } from "./paths.js";
|
|
15
|
+
import { jobBasename, packageRoot } from "./paths.js";
|
|
15
16
|
import { SANDBOX_PREFLIGHT_PREFIX } from "./sandbox.js";
|
|
16
|
-
import {
|
|
17
|
+
import { renderApp, renderMessagePage, renderTextPage } from "./serve-page.js";
|
|
18
|
+
import { isPaused, pauseFactory, resumeFactory, spendFor } from "./rails.js";
|
|
17
19
|
import { readSettings, saveSettings } from "./settings.js";
|
|
18
20
|
import { listWorkflowNames } from "./workflow.js";
|
|
19
21
|
const BODY_LIMIT = 65_536;
|
|
@@ -161,8 +163,18 @@ async function handle(req, res, state, boundPort, css, csrf) {
|
|
|
161
163
|
await createJob(req, res, state, raw);
|
|
162
164
|
return;
|
|
163
165
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
+
if (path === "/api/pause" || path === "/api/resume") {
|
|
167
|
+
const pausing = path === "/api/pause";
|
|
168
|
+
const changed = pausing ? pauseFactory(config.kilnDir) : resumeFactory(config.kilnDir);
|
|
169
|
+
if (wantsJson(req)) {
|
|
170
|
+
sendJson(res, 200, { ok: true, paused: pausing, changed }, secrets);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
redirect(res, redirectTarget(req));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const action = /^\/api\/jobs\/([^/]+)\/(approve|reject|retry|revise)$/.exec(path);
|
|
177
|
+
if (!action?.[1] || (action[2] !== "approve" && action[2] !== "reject" && action[2] !== "retry" && action[2] !== "revise")) {
|
|
166
178
|
sendJson(res, 404, { error: "Not found." }, secrets);
|
|
167
179
|
return;
|
|
168
180
|
}
|
|
@@ -175,6 +187,14 @@ async function handle(req, res, state, boundPort, css, csrf) {
|
|
|
175
187
|
await retry(req, res, config, css, secrets, jobId);
|
|
176
188
|
return;
|
|
177
189
|
}
|
|
190
|
+
if (action[2] === "revise") {
|
|
191
|
+
await revise(req, res, config, css, secrets, jobId, raw);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (action[2] !== "approve" && action[2] !== "reject") {
|
|
195
|
+
sendJson(res, 404, { error: "Not found." }, secrets);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
178
198
|
await answer(req, res, config, css, secrets, jobId, action[2]);
|
|
179
199
|
return;
|
|
180
200
|
}
|
|
@@ -182,14 +202,31 @@ async function handle(req, res, state, boundPort, css, csrf) {
|
|
|
182
202
|
sendJson(res, 405, { error: "Method not allowed." }, secrets);
|
|
183
203
|
return;
|
|
184
204
|
}
|
|
185
|
-
if (path === "/" || path === "" || path === "/board") {
|
|
186
|
-
sendHtml(res, 200, renderConsole(css, readStatus(config).attention, pageOptions(state, csrf)), secrets);
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
205
|
if (path === "/api/jobs" || path === "/api/status") {
|
|
190
206
|
sendJson(res, 200, statusDocument(state), secrets);
|
|
191
207
|
return;
|
|
192
208
|
}
|
|
209
|
+
if (path === "/api/board") {
|
|
210
|
+
sendJson(res, 200, boardPayload(state), secrets);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (path === "/api/stats") {
|
|
214
|
+
const window = parseStatsWindow(url.searchParams.get("window"));
|
|
215
|
+
if (!window) {
|
|
216
|
+
sendJson(res, 400, { error: "window must be today, 7d, or 30d." }, secrets);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
sendJson(res, 200, statsDocument(state, window), secrets);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (path === "/api/report") {
|
|
223
|
+
sendJson(res, 200, reportDocument(state), secrets);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (path === "/api/unseen") {
|
|
227
|
+
sendJson(res, 200, unseenDocument(state, url.searchParams.getAll("seen")), secrets);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
193
230
|
if (path === "/api/events") {
|
|
194
231
|
const jobId = url.searchParams.get("job_id");
|
|
195
232
|
if (!jobId || !JOB_ID.test(jobId)) {
|
|
@@ -199,6 +236,17 @@ async function handle(req, res, state, boundPort, css, csrf) {
|
|
|
199
236
|
sendJson(res, 200, { job_id: jobId, events: eventsForJob(config.kilnDir, jobId) }, secrets);
|
|
200
237
|
return;
|
|
201
238
|
}
|
|
239
|
+
const apiDetail = /^\/api\/jobs\/([^/]+)\/detail$/.exec(path);
|
|
240
|
+
if (apiDetail?.[1]) {
|
|
241
|
+
const jobId = parseJobId(apiDetail[1]);
|
|
242
|
+
const job = jobId ? findJob(config, jobId) : undefined;
|
|
243
|
+
if (!job) {
|
|
244
|
+
sendJson(res, 404, { error: jobId ? `No job ${jobId}.` : "Not found." }, secrets);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
sendJson(res, 200, detailDocument(state, job), secrets);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
202
250
|
const apiJob = /^\/api\/jobs\/([^/]+)$/.exec(path);
|
|
203
251
|
if (apiJob?.[1]) {
|
|
204
252
|
const jobId = parseJobId(apiJob[1]);
|
|
@@ -210,6 +258,54 @@ async function handle(req, res, state, boundPort, css, csrf) {
|
|
|
210
258
|
sendJson(res, 200, job, secrets);
|
|
211
259
|
return;
|
|
212
260
|
}
|
|
261
|
+
const listed = listSection(path);
|
|
262
|
+
if (listed) {
|
|
263
|
+
if (listed === "stats") {
|
|
264
|
+
const window = parseStatsWindow(url.searchParams.get("window"));
|
|
265
|
+
if (!window) {
|
|
266
|
+
sendHtml(res, 400, renderMessagePage(css, "Stats", "window must be today, 7d, or 30d."), secrets);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
sendHtml(res, 200, renderShell(state, css, csrf, "stats", { window }), secrets);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
sendHtml(res, 200, renderShell(state, css, csrf, listed), secrets);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const repoPage = /^\/repos\/([^/]+)$/.exec(path);
|
|
276
|
+
if (repoPage?.[1]) {
|
|
277
|
+
let name;
|
|
278
|
+
try {
|
|
279
|
+
name = decodeURIComponent(repoPage[1]);
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
sendHtml(res, 404, renderMessagePage(css, "Not found", "Not found."), secrets);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
if (!state.config.repos[name]) {
|
|
286
|
+
sendHtml(res, 404, renderMessagePage(css, "Not found", `No repo ${name}.`), secrets);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
sendHtml(res, 200, renderShell(state, css, csrf, "repos", { repo: name }), secrets);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const jobPage = /^\/jobs\/([^/]+)$/.exec(path);
|
|
293
|
+
if (jobPage?.[1]) {
|
|
294
|
+
const jobId = parseJobId(jobPage[1]);
|
|
295
|
+
const job = jobId ? findJob(config, jobId) : undefined;
|
|
296
|
+
if (!job) {
|
|
297
|
+
sendHtml(res, 404, renderMessagePage(css, "Not found", jobId ? `No job ${jobId}.` : "Not found."), secrets);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const tabRaw = url.searchParams.get("tab");
|
|
301
|
+
if (tabRaw && tabRaw !== "overview" && tabRaw !== "changes" && tabRaw !== "timeline") {
|
|
302
|
+
sendHtml(res, 400, renderMessagePage(css, "Job", "tab must be overview, changes, or timeline."), secrets);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const tab = tabRaw === "changes" || tabRaw === "timeline" ? tabRaw : "overview";
|
|
306
|
+
sendHtml(res, 200, renderShell(state, css, csrf, "job", { detail: detailDocument(state, job), tab }), secrets);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
213
309
|
const page = /^\/jobs\/([^/]+)\/(spec|log|findings)$/.exec(path);
|
|
214
310
|
if (page?.[1] && (page[2] === "spec" || page[2] === "log" || page[2] === "findings")) {
|
|
215
311
|
const jobId = parseJobId(page[1]);
|
|
@@ -261,6 +357,48 @@ async function answer(req, res, config, css, secrets, jobId, choice) {
|
|
|
261
357
|
}
|
|
262
358
|
redirect(res, redirectTarget(req));
|
|
263
359
|
}
|
|
360
|
+
async function revise(req, res, config, css, secrets, jobId, raw) {
|
|
361
|
+
const note = noteFromBody(req, raw);
|
|
362
|
+
if (!note) {
|
|
363
|
+
const message = "A revise note is required.";
|
|
364
|
+
if (wantsJson(req))
|
|
365
|
+
sendJson(res, 400, { error: message }, secrets);
|
|
366
|
+
else
|
|
367
|
+
sendHtml(res, 400, renderMessagePage(css, "Could not revise", message), secrets);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
try {
|
|
371
|
+
reviseJob(config, jobId, note);
|
|
372
|
+
}
|
|
373
|
+
catch (error) {
|
|
374
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
375
|
+
const status = message.startsWith("No job ") ? 404 : 409;
|
|
376
|
+
if (wantsJson(req))
|
|
377
|
+
sendJson(res, status, { error: message }, secrets);
|
|
378
|
+
else
|
|
379
|
+
sendHtml(res, status, renderMessagePage(css, "Could not revise", message), secrets);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
const job = findJob(config, jobId);
|
|
383
|
+
if (wantsJson(req)) {
|
|
384
|
+
sendJson(res, 200, { ok: true, job }, secrets);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
redirect(res, redirectTarget(req));
|
|
388
|
+
}
|
|
389
|
+
function noteFromBody(req, raw) {
|
|
390
|
+
const type = headerValue(req.headers["content-type"]);
|
|
391
|
+
if (type.includes("application/json")) {
|
|
392
|
+
try {
|
|
393
|
+
const parsed = JSON.parse(raw);
|
|
394
|
+
return typeof parsed.note === "string" ? parsed.note : undefined;
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
return undefined;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return new URLSearchParams(raw).get("note") ?? undefined;
|
|
401
|
+
}
|
|
264
402
|
async function retry(req, res, config, css, secrets, jobId) {
|
|
265
403
|
try {
|
|
266
404
|
retryJob(config, jobId);
|
|
@@ -341,13 +479,13 @@ function redirectTarget(req) {
|
|
|
341
479
|
return "/";
|
|
342
480
|
try {
|
|
343
481
|
const url = new URL(referer);
|
|
344
|
-
if (url.pathname
|
|
345
|
-
return "/
|
|
482
|
+
if (!url.pathname.startsWith("/") || url.pathname.startsWith("//"))
|
|
483
|
+
return "/";
|
|
484
|
+
return `${url.pathname}${url.search}`;
|
|
346
485
|
}
|
|
347
486
|
catch {
|
|
348
487
|
return "/";
|
|
349
488
|
}
|
|
350
|
-
return "/";
|
|
351
489
|
}
|
|
352
490
|
function wantsJson(req) {
|
|
353
491
|
const accept = headerValue(req.headers.accept);
|
|
@@ -561,20 +699,121 @@ function repoList(config) {
|
|
|
561
699
|
.sort((left, right) => left.name.localeCompare(right.name));
|
|
562
700
|
}
|
|
563
701
|
function pageOptions(state, csrf) {
|
|
702
|
+
const spend = spendFor(state.config.kilnDir, state.config.dailyDollarCap);
|
|
564
703
|
return {
|
|
565
704
|
csrf,
|
|
566
705
|
repos: repoList(state.config),
|
|
567
706
|
workflows: listWorkflowNames(),
|
|
707
|
+
paused: isPaused(state.config.kilnDir),
|
|
708
|
+
spend,
|
|
568
709
|
...(state.configError ? { configError: state.configError } : {}),
|
|
569
710
|
};
|
|
570
711
|
}
|
|
571
712
|
function statusDocument(state) {
|
|
713
|
+
const spend = spendFor(state.config.kilnDir, state.config.dailyDollarCap);
|
|
572
714
|
return {
|
|
573
715
|
...readStatus(state.config),
|
|
574
716
|
repos: repoList(state.config),
|
|
575
717
|
configError: state.configError ?? null,
|
|
718
|
+
paused: isPaused(state.config.kilnDir),
|
|
719
|
+
spend: { day: spend.day, usd: spend.usd, cap: spend.cap, blocked: spend.blocked },
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
function listSection(path) {
|
|
723
|
+
if (path === "/" || path === "" || path === "/board" || path === "/inbox")
|
|
724
|
+
return "inbox";
|
|
725
|
+
if (path === "/hand")
|
|
726
|
+
return "hand";
|
|
727
|
+
if (path === "/repos")
|
|
728
|
+
return "repos";
|
|
729
|
+
if (path === "/history")
|
|
730
|
+
return "history";
|
|
731
|
+
if (path === "/stats")
|
|
732
|
+
return "stats";
|
|
733
|
+
if (path === "/report")
|
|
734
|
+
return "report";
|
|
735
|
+
if (path === "/settings")
|
|
736
|
+
return "settings";
|
|
737
|
+
return undefined;
|
|
738
|
+
}
|
|
739
|
+
function renderShell(state, css, csrf, section, extra = {}) {
|
|
740
|
+
const status = readStatus(state.config);
|
|
741
|
+
const events = readEvents(state.config.kilnDir, { warn: () => { } });
|
|
742
|
+
const board = buildBoard(status.jobs, events);
|
|
743
|
+
const page = pageOptions(state, csrf);
|
|
744
|
+
return renderApp({
|
|
745
|
+
css,
|
|
746
|
+
section,
|
|
747
|
+
board,
|
|
748
|
+
attention: status.attention,
|
|
749
|
+
page: extra.repo ? { ...page, repo: extra.repo } : page,
|
|
750
|
+
...(extra.repo ? { repo: extra.repo } : {}),
|
|
751
|
+
...(section === "stats" && extra.window ? { stats: statsDocument(state, extra.window) } : {}),
|
|
752
|
+
...(section === "report" ? { report: reportDocument(state) } : {}),
|
|
753
|
+
...(extra.detail ? { detail: extra.detail, tab: extra.tab ?? "overview" } : {}),
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
function boardPayload(state) {
|
|
757
|
+
const status = readStatus(state.config);
|
|
758
|
+
const events = readEvents(state.config.kilnDir, { warn: () => { } });
|
|
759
|
+
const spend = spendFor(state.config.kilnDir, state.config.dailyDollarCap);
|
|
760
|
+
return {
|
|
761
|
+
...buildBoard(status.jobs, events),
|
|
762
|
+
paused: isPaused(state.config.kilnDir),
|
|
763
|
+
spend,
|
|
764
|
+
configError: state.configError ?? null,
|
|
576
765
|
};
|
|
577
766
|
}
|
|
767
|
+
function statsDocument(state, window) {
|
|
768
|
+
const events = readEvents(state.config.kilnDir, { warn: () => { } });
|
|
769
|
+
return statsFor(events, {
|
|
770
|
+
window,
|
|
771
|
+
...(state.config.dailyDollarCap !== undefined ? { cap: state.config.dailyDollarCap } : {}),
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
function reportDocument(state) {
|
|
775
|
+
const events = readEvents(state.config.kilnDir, { warn: () => { } });
|
|
776
|
+
return dayReport(events, state.config.dailyDollarCap !== undefined ? { cap: state.config.dailyDollarCap } : {});
|
|
777
|
+
}
|
|
778
|
+
function unseenDocument(state, seenValues) {
|
|
779
|
+
const events = readEvents(state.config.kilnDir, { warn: () => { } });
|
|
780
|
+
const board = buildBoard(readStatus(state.config).jobs, events);
|
|
781
|
+
return unseenChanges(events, parseSeenQuery(seenValues), new Set(board.inbox.map((job) => job.jobId)));
|
|
782
|
+
}
|
|
783
|
+
function detailDocument(state, job) {
|
|
784
|
+
const events = readEvents(state.config.kilnDir, { warn: () => { } });
|
|
785
|
+
const repo = state.config.repos[job.repo];
|
|
786
|
+
const branch = branchOf(job, events);
|
|
787
|
+
const base = prBase(events, job.jobId);
|
|
788
|
+
const commits = repo && branch ? readBranchCommits(repo.path, branch) : [];
|
|
789
|
+
const diff = repo && branch && base ? readDiffStat(repo.path, base, branch) : undefined;
|
|
790
|
+
const issue = jobIssue(state.config, job.jobId);
|
|
791
|
+
return jobDetail({
|
|
792
|
+
job,
|
|
793
|
+
events,
|
|
794
|
+
maxFixRounds: repo?.maxFixRounds ?? state.config.maxFixRounds,
|
|
795
|
+
commits,
|
|
796
|
+
...(issue ? { issue } : {}),
|
|
797
|
+
...(diff ? { diffStat: diff } : {}),
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
function prBase(events, jobId) {
|
|
801
|
+
let base;
|
|
802
|
+
for (const event of events) {
|
|
803
|
+
if (event.job_id === jobId && event.event === "pr.opened")
|
|
804
|
+
base = event.properties.base;
|
|
805
|
+
}
|
|
806
|
+
return base;
|
|
807
|
+
}
|
|
808
|
+
function jobIssue(config, jobId) {
|
|
809
|
+
try {
|
|
810
|
+
const raw = JSON.parse(readFileSync(join(config.jobsDir, `${jobBasename(jobId)}.json`), "utf8"));
|
|
811
|
+
return typeof raw.issue === "string" ? raw.issue : undefined;
|
|
812
|
+
}
|
|
813
|
+
catch {
|
|
814
|
+
return undefined;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
578
817
|
function statusCode(error) {
|
|
579
818
|
if (typeof error === "object" && error !== null && "statusCode" in error) {
|
|
580
819
|
const code = error.statusCode;
|
package/dist/settings.js
CHANGED
|
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { basename, dirname, join } from "node:path";
|
|
5
5
|
import { assertBranchName, assertCheckoutDirectory } from "./config.js";
|
|
6
6
|
import { DEFAULT_BRANCH_TEMPLATE, DEFAULT_PR_TITLE } from "./pr-names.js";
|
|
7
|
-
import { DEFAULT_MAX_ATTEMPTS, DEFAULT_MAX_FIX_ROUNDS, DEFAULT_STALL_TIMEOUT_MS, KilnConfigFileSchema, SettingsWriteSchema, } from "./schemas.js";
|
|
7
|
+
import { DEFAULT_MAX_ATTEMPTS, DEFAULT_MAX_FIX_ROUNDS, DEFAULT_MAX_SAME_FINGERPRINT, DEFAULT_STALL_TIMEOUT_MS, KilnConfigFileSchema, SettingsWriteSchema, } from "./schemas.js";
|
|
8
8
|
const DEFAULTS = {
|
|
9
9
|
deliver: "local",
|
|
10
10
|
prTitle: DEFAULT_PR_TITLE,
|
|
@@ -12,6 +12,8 @@ const DEFAULTS = {
|
|
|
12
12
|
maxAttempts: DEFAULT_MAX_ATTEMPTS,
|
|
13
13
|
stallTimeoutMs: DEFAULT_STALL_TIMEOUT_MS,
|
|
14
14
|
maxFixRounds: DEFAULT_MAX_FIX_ROUNDS,
|
|
15
|
+
maxSameFingerprint: DEFAULT_MAX_SAME_FINGERPRINT,
|
|
16
|
+
dailyDollarCap: null,
|
|
15
17
|
sandboxMode: "off",
|
|
16
18
|
};
|
|
17
19
|
export function readSettings(configPath) {
|
|
@@ -69,6 +71,8 @@ function project(path, revision, file) {
|
|
|
69
71
|
...(file.stallTimeoutMs !== undefined ? { stallTimeoutMs: file.stallTimeoutMs } : {}),
|
|
70
72
|
...(file.maxAttempts !== undefined ? { maxAttempts: file.maxAttempts } : {}),
|
|
71
73
|
...(file.maxFixRounds !== undefined ? { maxFixRounds: file.maxFixRounds } : {}),
|
|
74
|
+
...(file.maxSameFingerprint !== undefined ? { maxSameFingerprint: file.maxSameFingerprint } : {}),
|
|
75
|
+
...(file.dailyDollarCap !== undefined ? { dailyDollarCap: file.dailyDollarCap } : {}),
|
|
72
76
|
...(sandbox ? { sandbox } : {}),
|
|
73
77
|
defaults: DEFAULTS,
|
|
74
78
|
};
|
|
@@ -97,12 +101,16 @@ function mergeSettings(raw, settings) {
|
|
|
97
101
|
setOptional(prior, "base", entry.base);
|
|
98
102
|
setOptional(prior, "prTitle", entry.prTitle);
|
|
99
103
|
setOptional(prior, "branch", entry.branch);
|
|
104
|
+
setOptional(prior, "maxAttempts", entry.maxAttempts);
|
|
105
|
+
setOptional(prior, "maxFixRounds", entry.maxFixRounds);
|
|
100
106
|
repos[name] = prior;
|
|
101
107
|
}
|
|
102
108
|
next.repos = repos;
|
|
103
109
|
setOptional(next, "stallTimeoutMs", settings.stallTimeoutMs);
|
|
104
110
|
setOptional(next, "maxAttempts", settings.maxAttempts);
|
|
105
111
|
setOptional(next, "maxFixRounds", settings.maxFixRounds);
|
|
112
|
+
setOptional(next, "maxSameFingerprint", settings.maxSameFingerprint);
|
|
113
|
+
setOptional(next, "dailyDollarCap", settings.dailyDollarCap);
|
|
106
114
|
if (settings.sandbox) {
|
|
107
115
|
const prior = isRecord(raw.sandbox) ? { ...raw.sandbox } : {};
|
|
108
116
|
setOptional(prior, "mode", settings.sandbox.mode);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lchase/kiln",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Run a workflow on any registered repo. A markdown spec is the input.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
],
|
|
17
17
|
"repository": {
|
|
18
18
|
"type": "git",
|
|
19
|
-
"url": "git+https://github.com/lchase/kiln.git"
|
|
19
|
+
"url": "git+https://github.com/lchase/kiln.git",
|
|
20
|
+
"directory": "packages/kiln"
|
|
20
21
|
},
|
|
21
22
|
"publishConfig": {
|
|
22
23
|
"access": "public"
|
|
@@ -34,7 +35,7 @@
|
|
|
34
35
|
"node": ">=22"
|
|
35
36
|
},
|
|
36
37
|
"dependencies": {
|
|
37
|
-
"@lchase/yak": "0.
|
|
38
|
+
"@lchase/yak": "0.9.0",
|
|
38
39
|
"zod": "^4.6.7"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
package/workflows/fix.yaml
CHANGED
|
@@ -45,4 +45,4 @@ steps:
|
|
|
45
45
|
needs: [input, build]
|
|
46
46
|
skipIf: 'build.status != "READY"'
|
|
47
47
|
command:
|
|
48
|
-
run: 'git add -A && git diff --cached --quiet || git commit -m "kiln: $KILN_TASK_TITLE"'
|
|
48
|
+
run: 'git add -A && git diff --cached --quiet || git commit -m "kiln: $KILN_TASK_TITLE" --trailer "Kiln-Job: $KILN_JOB" --trailer "Kiln-Round: $KILN_ROUND" --trailer "Kiln-Prompts-Hash: $KILN_PROMPTS_HASH"'
|