@lchase/kiln 0.12.0 → 0.14.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 +14 -3
- package/assets/serve.css +136 -11
- package/assets/serve.js +44 -5
- package/dist/attention.js +5 -1
- package/dist/board-view.js +4 -162
- package/dist/cli.js +51 -3
- package/dist/config.js +1 -0
- package/dist/driver.js +63 -9
- package/dist/events.js +36 -1
- package/dist/host.js +98 -3
- package/dist/job-view.js +755 -0
- package/dist/jobs.js +9 -0
- package/dist/markdown.js +91 -0
- package/dist/project.js +26 -1
- package/dist/reconcile.js +5 -2
- package/dist/report.js +8 -0
- package/dist/review-post.js +269 -0
- package/dist/schemas.js +21 -1
- package/dist/serve-page.js +278 -69
- package/dist/serve.js +46 -2
- package/dist/settings.js +4 -2
- package/dist/triggers.js +232 -23
- package/package.json +1 -1
- package/prompts/kiln/review.md +1 -1
- package/workflows/review.yaml +4 -4
package/dist/triggers.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { loadKilnConfig } from "./config.js";
|
|
4
5
|
import { appendEvent, readEvents } from "./events.js";
|
|
5
6
|
import { isIgnoredAuthor } from "./feedback.js";
|
|
7
|
+
import { DEFAULT_REVIEW_LABEL } from "./host.js";
|
|
6
8
|
import { searchJiraIssues } from "./intake/jira.js";
|
|
7
9
|
import { listValidJobs, writeJob } from "./jobs.js";
|
|
10
|
+
import { jobBasename } from "./paths.js";
|
|
8
11
|
import { pausedFrom, spendSnapshot } from "./rails.js";
|
|
9
12
|
import { redact } from "./redact.js";
|
|
13
|
+
import { isPidAlive } from "./reliability.js";
|
|
10
14
|
import { ISSUE_KEY } from "./schemas.js";
|
|
11
15
|
import { REVIEW_WORKFLOW } from "./workflow.js";
|
|
12
16
|
/**
|
|
@@ -14,7 +18,8 @@ import { REVIEW_WORKFLOW } from "./workflow.js";
|
|
|
14
18
|
* A trigger writes a typed event. The router creates a job or attaches to
|
|
15
19
|
* one. The dedupe key lives on that event, so a restart does not create
|
|
16
20
|
* the job twice. A failed fire records `last_error` and waits for the next
|
|
17
|
-
* slot. Pause
|
|
21
|
+
* slot. Pause, the daily cap, and the review burst caps record a skip.
|
|
22
|
+
* Nothing over the cap is queued.
|
|
18
23
|
*
|
|
19
24
|
* Jira is polled. `jiraIssueEvent` is the seam a webhook can call later.
|
|
20
25
|
* Pull-request activity reuses the feedback loop's snapshot. It does not
|
|
@@ -25,8 +30,20 @@ export const PR_ACTIVITY_TRIGGER = "pr-activity";
|
|
|
25
30
|
/** Minute 0 of every hour, local time. */
|
|
26
31
|
export const DEFAULT_REVIEW_CRON = "0 * * * *";
|
|
27
32
|
export const DEFAULT_JIRA_EVERY = "5m";
|
|
33
|
+
/** New review jobs one fire may create. Further pull requests are skipped. */
|
|
34
|
+
export const DEFAULT_REVIEW_MAX_NEW = 3;
|
|
35
|
+
/** Queued or running review jobs at once. A review waiting in the Inbox does not count. */
|
|
36
|
+
export const DEFAULT_REVIEW_MAX_QUEUED = 5;
|
|
28
37
|
const BUILTIN_TRIGGERS = [
|
|
29
|
-
{
|
|
38
|
+
{
|
|
39
|
+
id: REVIEW_REQUESTS_TRIGGER,
|
|
40
|
+
kind: "schedule",
|
|
41
|
+
cron: DEFAULT_REVIEW_CRON,
|
|
42
|
+
enabled: true,
|
|
43
|
+
label: DEFAULT_REVIEW_LABEL,
|
|
44
|
+
maxNew: DEFAULT_REVIEW_MAX_NEW,
|
|
45
|
+
maxQueued: DEFAULT_REVIEW_MAX_QUEUED,
|
|
46
|
+
},
|
|
30
47
|
{ id: PR_ACTIVITY_TRIGGER, kind: "pr", enabled: true },
|
|
31
48
|
];
|
|
32
49
|
const BUILTIN_ROUTES = [
|
|
@@ -34,6 +51,16 @@ const BUILTIN_ROUTES = [
|
|
|
34
51
|
{ id: PR_ACTIVITY_TRIGGER, trigger: PR_ACTIVITY_TRIGGER, action: "attach" },
|
|
35
52
|
];
|
|
36
53
|
const REVIEW_REF = /^review:([^:]+):(\d+):([0-9a-f]{7,64})$/i;
|
|
54
|
+
/** `review:<repo>:<number>:<head sha>` from a review-request job. SHA is lowercase. */
|
|
55
|
+
export function parseReviewSource(ref) {
|
|
56
|
+
const match = REVIEW_REF.exec(ref);
|
|
57
|
+
const repo = match?.[1];
|
|
58
|
+
const number = Number(match?.[2]);
|
|
59
|
+
const sha = match?.[3]?.toLowerCase();
|
|
60
|
+
if (!repo || !sha || !Number.isInteger(number) || number <= 0)
|
|
61
|
+
return undefined;
|
|
62
|
+
return { repo, number, sha };
|
|
63
|
+
}
|
|
37
64
|
export function effectiveTriggers(file) {
|
|
38
65
|
const byId = new Map();
|
|
39
66
|
for (const trigger of BUILTIN_TRIGGERS)
|
|
@@ -234,7 +261,7 @@ export function jiraIssueEvent(triggerId, repo, issue) {
|
|
|
234
261
|
export function reviewHeadOf(job) {
|
|
235
262
|
if (job.workflow !== REVIEW_WORKFLOW || job.source?.kind !== "trigger")
|
|
236
263
|
return undefined;
|
|
237
|
-
return
|
|
264
|
+
return parseReviewSource(job.source.ref)?.sha;
|
|
238
265
|
}
|
|
239
266
|
/** Download the pull request head when the checkout does not have that commit yet. */
|
|
240
267
|
export function ensureReviewHead(checkout, prNumber, sha) {
|
|
@@ -304,11 +331,21 @@ export function triggerRows(config, events, now = new Date()) {
|
|
|
304
331
|
event: event.event,
|
|
305
332
|
summary: triggerSummary(event),
|
|
306
333
|
}));
|
|
334
|
+
const review = trigger.id === REVIEW_REQUESTS_TRIGGER;
|
|
335
|
+
const repoLabels = review ? repoReviewLabels(config) : [];
|
|
307
336
|
return {
|
|
308
337
|
id: trigger.id,
|
|
309
338
|
kind: trigger.kind,
|
|
310
339
|
enabled: trigger.enabled !== false,
|
|
311
340
|
schedule: scheduleLabel(trigger),
|
|
341
|
+
...(review
|
|
342
|
+
? {
|
|
343
|
+
label: reviewRequestLabel(trigger),
|
|
344
|
+
maxNew: reviewMaxNew(trigger),
|
|
345
|
+
maxQueued: reviewMaxQueued(trigger),
|
|
346
|
+
...(repoLabels.length > 0 ? { repoLabels } : {}),
|
|
347
|
+
}
|
|
348
|
+
: {}),
|
|
312
349
|
...(nextFire(trigger, state, now) ? { nextFire: nextFire(trigger, state, now) } : {}),
|
|
313
350
|
...(state?.lastFire && Number.isFinite(Date.parse(state.lastFire)) ? { lastFire: state.lastFire } : {}),
|
|
314
351
|
...(state?.lastError ? { lastError: state.lastError } : {}),
|
|
@@ -336,14 +373,21 @@ function runSchedule(input, trigger, routes, now, startup) {
|
|
|
336
373
|
if (reviews) {
|
|
337
374
|
if (!input.reviews)
|
|
338
375
|
return;
|
|
376
|
+
// A disable that landed after this process started, including during the
|
|
377
|
+
// startup list, must not create jobs. The Triggers page reads the file,
|
|
378
|
+
// so it can already say disabled while this fire is still in the list.
|
|
379
|
+
if (!triggerEnabledNow(input.config, trigger.id))
|
|
380
|
+
return;
|
|
339
381
|
let requests;
|
|
340
382
|
try {
|
|
341
|
-
requests = input.reviews.list(
|
|
383
|
+
requests = input.reviews.list(reviewTargets(input.config, trigger));
|
|
342
384
|
}
|
|
343
385
|
catch (error) {
|
|
344
386
|
failSlot(input.config, trigger.id, stamp, errorMessage(error), now);
|
|
345
387
|
return;
|
|
346
388
|
}
|
|
389
|
+
if (!triggerEnabledNow(input.config, trigger.id))
|
|
390
|
+
return;
|
|
347
391
|
succeedSlot(input.config.kilnDir, trigger.id, stamp);
|
|
348
392
|
appendFire(input.config, {
|
|
349
393
|
key: `schedule:${trigger.id}:${stamp}`,
|
|
@@ -352,17 +396,40 @@ function runSchedule(input, trigger, routes, now, startup) {
|
|
|
352
396
|
id: stamp,
|
|
353
397
|
revision: stamp,
|
|
354
398
|
});
|
|
399
|
+
const maxNew = reviewMaxNew(trigger);
|
|
400
|
+
const maxQueued = reviewMaxQueued(trigger);
|
|
401
|
+
let created = 0;
|
|
402
|
+
let active = reviewSlotsInUse(input.config);
|
|
355
403
|
for (const request of requests) {
|
|
404
|
+
if (!triggerEnabledNow(input.config, trigger.id))
|
|
405
|
+
break;
|
|
406
|
+
const key = reviewDedupeKey(request);
|
|
407
|
+
if (reviewDrafted(input.config, key)) {
|
|
408
|
+
try {
|
|
409
|
+
ingestReview(input.config, request, routes, now);
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
noteReviewFailure(input.config, trigger.id, stamp, request, error, now);
|
|
413
|
+
}
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
if (active >= maxQueued) {
|
|
417
|
+
noteSkip(input.config, trigger.id, "queued-cap", stamp, now, key);
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (created >= maxNew) {
|
|
421
|
+
noteSkip(input.config, trigger.id, "burst-cap", stamp, now, key);
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
356
424
|
try {
|
|
357
|
-
ingestReview(input.config, request, routes, now);
|
|
425
|
+
const outcome = ingestReview(input.config, request, routes, now);
|
|
426
|
+
if (outcome === "created") {
|
|
427
|
+
created += 1;
|
|
428
|
+
active += 1;
|
|
429
|
+
}
|
|
358
430
|
}
|
|
359
431
|
catch (error) {
|
|
360
|
-
|
|
361
|
-
lastFire: stamp,
|
|
362
|
-
lastError: publicError(errorMessage(error)),
|
|
363
|
-
lastErrorAt: now.toISOString(),
|
|
364
|
-
});
|
|
365
|
-
appendFailed(input.config, trigger.id, publicError(errorMessage(error)), `review:${request.repo}:${request.number}`, now);
|
|
432
|
+
noteReviewFailure(input.config, trigger.id, stamp, request, error, now);
|
|
366
433
|
}
|
|
367
434
|
}
|
|
368
435
|
return;
|
|
@@ -390,8 +457,8 @@ function ingestReview(config, request, routes, now) {
|
|
|
390
457
|
const base = repo.base ?? "the default branch";
|
|
391
458
|
const url = httpUrl(request.url);
|
|
392
459
|
const title = oneLine(request.title) || `PR ${request.number}`;
|
|
393
|
-
ingest(config, {
|
|
394
|
-
key:
|
|
460
|
+
return ingest(config, {
|
|
461
|
+
key: reviewDedupeKey(request),
|
|
395
462
|
source: "review",
|
|
396
463
|
triggerId: REVIEW_REQUESTS_TRIGGER,
|
|
397
464
|
id: String(request.number),
|
|
@@ -455,20 +522,20 @@ function ingest(config, event, routes, now) {
|
|
|
455
522
|
const routed = routedKeys(events);
|
|
456
523
|
const decision = routeTrigger(event, routes, routed);
|
|
457
524
|
if (decision.type === "ignore" || decision.type === "deduped")
|
|
458
|
-
return;
|
|
525
|
+
return "ignored";
|
|
459
526
|
if (decision.type === "unrouted") {
|
|
460
527
|
noteConfigError(config, event.triggerId, `No route for trigger ${event.triggerId}.`, now);
|
|
461
|
-
return;
|
|
528
|
+
return "ignored";
|
|
462
529
|
}
|
|
463
530
|
appendFire(config, event, now);
|
|
464
531
|
if (decision.type === "attach") {
|
|
465
532
|
appendRouted(config, event, decision.jobId, "attach", undefined, now);
|
|
466
|
-
return;
|
|
533
|
+
return "existing";
|
|
467
534
|
}
|
|
468
535
|
const existing = listValidJobs(config).find((job) => job.source?.kind === "trigger" && job.source.ref === event.key);
|
|
469
536
|
if (existing) {
|
|
470
537
|
appendRouted(config, event, existing.id, "create", decision.workflow, now);
|
|
471
|
-
return;
|
|
538
|
+
return "existing";
|
|
472
539
|
}
|
|
473
540
|
const title = oneLine(event.title ?? "") || event.id;
|
|
474
541
|
const specText = redact(event.body?.trim() || `# ${title}\n`);
|
|
@@ -484,13 +551,14 @@ function ingest(config, event, routes, now) {
|
|
|
484
551
|
now,
|
|
485
552
|
});
|
|
486
553
|
appendRouted(config, event, written.job.id, "create", decision.workflow, now);
|
|
554
|
+
return "created";
|
|
487
555
|
}
|
|
488
556
|
function reviewSpec(input) {
|
|
489
557
|
const lines = [`# Draft review: ${input.title}`, ""];
|
|
490
558
|
if (input.url)
|
|
491
559
|
lines.push(`Pull request: ${input.url}`);
|
|
492
560
|
lines.push(`Number: ${input.number}`, `Head: ${input.sha}`, `Base: ${input.base}`, "");
|
|
493
|
-
lines.push("Read the change and return a draft review.", "Do not post, approve, merge, or push.", "Approving the gate
|
|
561
|
+
lines.push("Read the change and return a draft review.", "Do not post, approve, merge, or push.", "Approving the gate posts this draft as a pull request comment. Kiln does not approve the pull request or merge it.", "");
|
|
494
562
|
return lines.join("\n");
|
|
495
563
|
}
|
|
496
564
|
function appendFire(config, event, now) {
|
|
@@ -545,7 +613,7 @@ function failSlot(config, triggerId, stamp, message, now) {
|
|
|
545
613
|
function succeedSlot(kilnDir, triggerId, stamp) {
|
|
546
614
|
remember(kilnDir, triggerId, { lastFire: stamp, lastError: undefined, lastErrorAt: undefined });
|
|
547
615
|
}
|
|
548
|
-
function noteSkip(config, triggerId, reason, stamp, now) {
|
|
616
|
+
function noteSkip(config, triggerId, reason, stamp, now, externalId) {
|
|
549
617
|
appendEvent(config.kilnDir, {
|
|
550
618
|
job_id: null,
|
|
551
619
|
event: "trigger.skipped",
|
|
@@ -553,10 +621,21 @@ function noteSkip(config, triggerId, reason, stamp, now) {
|
|
|
553
621
|
properties: {
|
|
554
622
|
trigger_id: triggerId,
|
|
555
623
|
reason,
|
|
556
|
-
|
|
624
|
+
...(externalId ? { external_id: externalId } : {}),
|
|
625
|
+
once: externalId
|
|
626
|
+
? `skipped:${triggerId}:${stamp}:${reason}:${externalId}`
|
|
627
|
+
: `skipped:${triggerId}:${stamp}:${reason}`,
|
|
557
628
|
},
|
|
558
629
|
});
|
|
559
630
|
}
|
|
631
|
+
function noteReviewFailure(config, triggerId, stamp, request, error, now) {
|
|
632
|
+
remember(config.kilnDir, triggerId, {
|
|
633
|
+
lastFire: stamp,
|
|
634
|
+
lastError: publicError(errorMessage(error)),
|
|
635
|
+
lastErrorAt: now.toISOString(),
|
|
636
|
+
});
|
|
637
|
+
appendFailed(config, triggerId, publicError(errorMessage(error)), `review:${request.repo}:${request.number}`, now);
|
|
638
|
+
}
|
|
560
639
|
function noteConfigError(config, triggerId, message, now) {
|
|
561
640
|
const error = publicError(message);
|
|
562
641
|
const current = readRuntime(config.kilnDir)[triggerId];
|
|
@@ -565,6 +644,133 @@ function noteConfigError(config, triggerId, message, now) {
|
|
|
565
644
|
remember(config.kilnDir, triggerId, { lastError: error, lastErrorAt: now.toISOString() });
|
|
566
645
|
appendFailed(config, triggerId, error, `config:${error}`, now);
|
|
567
646
|
}
|
|
647
|
+
/** GitHub label for a review-requests fire. Omitted config uses {@link DEFAULT_REVIEW_LABEL}. */
|
|
648
|
+
export function reviewRequestLabel(trigger) {
|
|
649
|
+
return trigger.label?.trim() || DEFAULT_REVIEW_LABEL;
|
|
650
|
+
}
|
|
651
|
+
export function reviewMaxNew(trigger) {
|
|
652
|
+
return trigger.maxNew ?? DEFAULT_REVIEW_MAX_NEW;
|
|
653
|
+
}
|
|
654
|
+
export function reviewMaxQueued(trigger) {
|
|
655
|
+
return trigger.maxQueued ?? DEFAULT_REVIEW_MAX_QUEUED;
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* Review jobs that are queued or running.
|
|
659
|
+
* A review sitting on the Inbox gate has a finished journal and does not count.
|
|
660
|
+
* A cancelled job does not count.
|
|
661
|
+
*/
|
|
662
|
+
export function reviewSlotsInUse(config) {
|
|
663
|
+
let count = 0;
|
|
664
|
+
for (const job of listValidJobs(config)) {
|
|
665
|
+
if (job.workflow !== REVIEW_WORKFLOW || job.cancelledAt)
|
|
666
|
+
continue;
|
|
667
|
+
if (reviewOccupiesSlot(config, job))
|
|
668
|
+
count += 1;
|
|
669
|
+
}
|
|
670
|
+
return count;
|
|
671
|
+
}
|
|
672
|
+
function reviewTargets(config, trigger) {
|
|
673
|
+
const fallback = reviewRequestLabel(trigger);
|
|
674
|
+
return Object.entries(config.repos).map(([name, repo]) => ({
|
|
675
|
+
name,
|
|
676
|
+
path: repo.path,
|
|
677
|
+
label: repo.reviewLabel?.trim() || fallback,
|
|
678
|
+
}));
|
|
679
|
+
}
|
|
680
|
+
function repoReviewLabels(config) {
|
|
681
|
+
const labels = [];
|
|
682
|
+
for (const [repo, entry] of Object.entries(config.repos)) {
|
|
683
|
+
const label = entry.reviewLabel?.trim();
|
|
684
|
+
if (label)
|
|
685
|
+
labels.push({ repo, label });
|
|
686
|
+
}
|
|
687
|
+
return labels;
|
|
688
|
+
}
|
|
689
|
+
function reviewDedupeKey(request) {
|
|
690
|
+
return `review:${request.repo}:${request.number}:${request.headSha.trim().toLowerCase()}`;
|
|
691
|
+
}
|
|
692
|
+
function reviewDrafted(config, key) {
|
|
693
|
+
if (routedKeys(readEvents(config.kilnDir, { warn: () => { } })).has(key))
|
|
694
|
+
return true;
|
|
695
|
+
return listValidJobs(config).some((job) => job.source?.kind === "trigger" && job.source.ref === key);
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* True when the trigger is enabled in memory and on disk.
|
|
699
|
+
* The disk read catches a disable that arrived while `gh pr list` or a fetch was running.
|
|
700
|
+
*/
|
|
701
|
+
function triggerEnabledNow(config, id) {
|
|
702
|
+
if (!triggerIsEnabled(config.triggers, id))
|
|
703
|
+
return false;
|
|
704
|
+
try {
|
|
705
|
+
return triggerIsEnabled(loadKilnConfig(config.configPath).triggers, id);
|
|
706
|
+
}
|
|
707
|
+
catch {
|
|
708
|
+
return true;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
function triggerIsEnabled(file, id) {
|
|
712
|
+
const trigger = effectiveTriggers(file).find((item) => item.id === id);
|
|
713
|
+
return trigger?.enabled !== false;
|
|
714
|
+
}
|
|
715
|
+
function reviewOccupiesSlot(config, job) {
|
|
716
|
+
if (launchingAlive(config.kilnDir, job.id))
|
|
717
|
+
return true;
|
|
718
|
+
const repo = config.repos[job.repo];
|
|
719
|
+
if (!repo)
|
|
720
|
+
return true;
|
|
721
|
+
const runs = journalsForJob(repo.path, job.id);
|
|
722
|
+
if (runs.length === 0)
|
|
723
|
+
return !deadLaunch(config.kilnDir, job.id);
|
|
724
|
+
return runs.some((run) => !run.finished);
|
|
725
|
+
}
|
|
726
|
+
function launchingAlive(kilnDir, jobId) {
|
|
727
|
+
const path = join(kilnDir, "launching", `${jobBasename(jobId)}.json`);
|
|
728
|
+
if (!existsSync(path))
|
|
729
|
+
return false;
|
|
730
|
+
try {
|
|
731
|
+
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
732
|
+
return typeof parsed.pid === "number" && isPidAlive(parsed.pid);
|
|
733
|
+
}
|
|
734
|
+
catch {
|
|
735
|
+
return false;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
function deadLaunch(kilnDir, jobId) {
|
|
739
|
+
const path = join(kilnDir, "launching", `${jobBasename(jobId)}.json`);
|
|
740
|
+
if (!existsSync(path))
|
|
741
|
+
return false;
|
|
742
|
+
return !launchingAlive(kilnDir, jobId);
|
|
743
|
+
}
|
|
744
|
+
function journalsForJob(checkout, jobId) {
|
|
745
|
+
const runsDir = join(checkout, ".runs");
|
|
746
|
+
if (!existsSync(runsDir))
|
|
747
|
+
return [];
|
|
748
|
+
const tag = `"kiln:${jobId}"`;
|
|
749
|
+
let names;
|
|
750
|
+
try {
|
|
751
|
+
names = readdirSync(runsDir);
|
|
752
|
+
}
|
|
753
|
+
catch {
|
|
754
|
+
return [];
|
|
755
|
+
}
|
|
756
|
+
const found = [];
|
|
757
|
+
for (const name of names) {
|
|
758
|
+
const file = join(runsDir, name, "journal.jsonl");
|
|
759
|
+
if (!existsSync(file))
|
|
760
|
+
continue;
|
|
761
|
+
let text;
|
|
762
|
+
try {
|
|
763
|
+
text = readFileSync(file, "utf8");
|
|
764
|
+
}
|
|
765
|
+
catch {
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
if (!text.includes(tag))
|
|
769
|
+
continue;
|
|
770
|
+
found.push({ finished: /"t"\s*:\s*"run\.finished"/.test(text) });
|
|
771
|
+
}
|
|
772
|
+
return found;
|
|
773
|
+
}
|
|
568
774
|
function blockReason(config, now) {
|
|
569
775
|
const events = readEvents(config.kilnDir, { warn: () => { } });
|
|
570
776
|
if (pausedFrom(events))
|
|
@@ -591,8 +797,11 @@ function isTriggerEvent(event) {
|
|
|
591
797
|
function triggerSummary(event) {
|
|
592
798
|
if (event.event === "trigger.failed")
|
|
593
799
|
return event.properties.error;
|
|
594
|
-
if (event.event === "trigger.skipped")
|
|
595
|
-
return event.properties.
|
|
800
|
+
if (event.event === "trigger.skipped") {
|
|
801
|
+
return event.properties.external_id
|
|
802
|
+
? `${event.properties.reason} ${event.properties.external_id}`
|
|
803
|
+
: event.properties.reason;
|
|
804
|
+
}
|
|
596
805
|
if (event.event === "trigger.routed")
|
|
597
806
|
return `${event.properties.action} ${event.properties.dedupe}`;
|
|
598
807
|
if (event.event === "trigger.fired")
|
package/package.json
CHANGED
package/prompts/kiln/review.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Kiln runs this review inside yak. The working directory is an isolated worktree at `{{input.baseRef}}`. That commit is the pull request head. Do not guess a branch name.
|
|
2
2
|
|
|
3
|
-
Read the change. Return a draft review and nothing else. Do not post the review, approve the pull request, merge it, push, or comment. Do not change product code. Do not create, modify, or delete files. Read-only git and file inspection are allowed.
|
|
3
|
+
Read the change. Return a draft review and nothing else. Do not post the review, approve the pull request, merge it, push, or comment. Kiln posts the draft only after a person approves the gate, and only as a comment. Do not change product code. Do not create, modify, or delete files. Read-only git and file inspection are allowed.
|
|
4
4
|
|
|
5
5
|
Return JSON with `markdown` and `findings`. `findings` is a list. Each item has `severity` (`note`, `nit`, `warning`, or `blocking`), `file`, `line`, and `comment`. `comment` is the text a person might post later. `markdown` is the whole draft a person will read, and it must include every finding with its severity, file, line, and suggested comment. If you find nothing, return an empty `findings` list and say so in `markdown`.
|
|
6
6
|
|
package/workflows/review.yaml
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# Draft review of one pull request, run by yak (@lchase/yak).
|
|
2
2
|
#
|
|
3
3
|
# The agent returns findings (severity, file, line, suggested comment).
|
|
4
|
-
# A person approves or rejects the draft in the Inbox. Approving
|
|
5
|
-
#
|
|
6
|
-
#
|
|
4
|
+
# A person approves or rejects the draft in the Inbox. Approving posts the
|
|
5
|
+
# draft as a pull request comment. Kiln does not approve the pull request
|
|
6
|
+
# or merge it. Rejecting or revising does not post.
|
|
7
7
|
#
|
|
8
8
|
# `reviewPrompt` in the kiln config replaces prompts/kiln/review.md.
|
|
9
9
|
# The packaged file is the generic default.
|
|
@@ -60,5 +60,5 @@ steps:
|
|
|
60
60
|
decision: { type: string, enum: [approve, reject] }
|
|
61
61
|
required: [decision]
|
|
62
62
|
render:
|
|
63
|
-
inline: "Draft review for \"{{input.title}}\"?\n\nApproving
|
|
63
|
+
inline: "Draft review for \"{{input.title}}\"?\n\nApproving posts this draft as a pull request comment. Kiln does not approve the pull request or merge it.\n\n{{draft.markdown}}"
|
|
64
64
|
produces: approval
|