@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/README.md
CHANGED
|
@@ -271,13 +271,23 @@ Two triggers are built in. `review-requests` is a schedule, cron `0 * * * *` (mi
|
|
|
271
271
|
}
|
|
272
272
|
```
|
|
273
273
|
|
|
274
|
-
`every` defaults to `5m` (`30s`, `5m`, `1h`). `label` and `status` compile to JQL. A `jql` string replaces them. `enabled` defaults to true. The Triggers page and `PUT /api/settings` send `{ "triggers": [{ "id", "enabled" }] }` and leave cron, JQL, and repo on the file.
|
|
274
|
+
`every` defaults to `5m` (`30s`, `5m`, `1h`). `label` and `status` compile to JQL. A `jql` string replaces them. `enabled` defaults to true. The Triggers page and `PUT /api/settings` send `{ "triggers": [{ "id", "enabled" }] }` and leave cron, JQL, label, and repo on the file. The Triggers page shows each trigger's schedule, whether it is enabled, and, for `review-requests`, the label and the two caps. Times on that page are the browser's local time.
|
|
275
275
|
|
|
276
276
|
A cron fires once per slot. While serve was down, startup fires the latest missed slot once and does not replay the ones behind it. A Jira poll that was missed does the same: one search, not one per skipped interval. The router writes `trigger.routed` with a `dedupe` property (`review:<repo>:<number>:<head sha>`, `jira:<trigger>:<issue>`, `pr:<job>:<comment>`). A later tick that sees that value does not create another job. Comments and commits from `kiln`, a `[bot]` login, a `kiln:` subject, or a `Kiln-Job` trailer are ignored.
|
|
277
277
|
|
|
278
|
-
A failed fire stores `last_error` and `lastErrorAt` under `<kiln-home>/triggers/state.json`, appends `trigger.failed`, and waits for the next slot. It does not retry on the next tick. Pause and the daily dollar cap append `trigger.skipped` with reason `paused` or `capped` and do not queue the fire.
|
|
278
|
+
A failed fire stores `last_error` and `lastErrorAt` under `<kiln-home>/triggers/state.json`, appends `trigger.failed`, and waits for the next slot. It does not retry on the next tick. Pause and the daily dollar cap append `trigger.skipped` with reason `paused` or `capped` and do not queue the fire. A review over the per-fire cap is `burst-cap`. A review that would pass the number of queued or running review jobs is `queued-cap`. Those two are not queued, so the next fire can pick them up. A disable is read again after the pull-request list returns and before each job is created, including the startup fire, so a trigger that is already disabled does not fire.
|
|
279
279
|
|
|
280
|
-
`review-requests` lists open pull requests where your `gh` user is the requested reviewer (`gh pr list --search review-requested:@me` in each registered GitHub repo). A pull request already drafted at that head SHA is skipped. The `review` workflow returns findings (severity, file, line, suggested comment) and stops on a gate in the Inbox. Approving
|
|
280
|
+
`review-requests` lists open pull requests where your `gh` user is the requested reviewer and the pull request carries a label (`gh pr list --search 'review-requested:@me label:"kiln-review"'` in each registered GitHub repo). The label defaults to `kiln-review` when `triggers[].label` is omitted. That is opt-in: unlabeled pull requests are not picked. Set `label` on the `review-requests` trigger to use a different one. `repos.<name>.reviewLabel` overrides it for that checkout. One fire creates at most `maxNew` new review jobs (default 3). At most `maxQueued` review jobs may be queued or running at once (default 5). A review waiting on the Inbox gate does not count. A pull request already drafted at that head SHA is skipped and does not use a slot. The `review` workflow returns findings (severity, file, line, suggested comment) and stops on a gate in the Inbox. Approving that gate posts the draft as a pull request comment (`COMMENT` only) under your `gh` user: the markdown is the body, and findings with a file and line are inline comments. Kiln does not approve the pull request or merge it. Rejecting or revising the gate does not post. Pause and the daily cap do not post. Posting the same repo, pull request, and head SHA again does not create a second review. A failed post stays in Needs a hand with `review.failed` and does not count as posted. `reviewPrompt` is a path to your own prompt file. Omitted, Kiln uses `prompts/kiln/review.md`.
|
|
281
|
+
|
|
282
|
+
```json
|
|
283
|
+
{
|
|
284
|
+
"triggers": [
|
|
285
|
+
{ "id": "review-requests", "kind": "schedule", "label": "kiln-review", "maxNew": 3, "maxQueued": 5 }
|
|
286
|
+
]
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
`kiln cancel --queued` cancels jobs that have not started. `--repo` and `--workflow` narrow it. `POST /api/jobs/cancel` with `{ "queued": true }` does the same for the board. Both are idempotent: a second call cancels nothing new and writes no second `job.cancelled` event. Running, blocked, and finished jobs stay. Cancelling does not delete branches.
|
|
281
291
|
|
|
282
292
|
Jira search sends `JIRA_API_TOKEN` only as a Basic header in a child process. The token is not written into the job, the spec, or the event log. Polling is the only Jira ingress. A webhook can later call the same issue-to-event function.
|
|
283
293
|
|
|
@@ -311,6 +321,7 @@ State is derived from the yak journal, the gate files, and (for `deliver=pr`) `g
|
|
|
311
321
|
| State | Meaning |
|
|
312
322
|
| --- | --- |
|
|
313
323
|
| `queued` | Dependencies are met, and the job has not started |
|
|
324
|
+
| `cancelled` | Cancelled before it started. Tick does not launch it. No branch is deleted |
|
|
314
325
|
| `blocked` | An after-merged dependency is not `done`, or a stacked dependency has not landed. A missing job id is named |
|
|
315
326
|
| `running` | Yak is alive, or the launch process is alive and has not written a journal |
|
|
316
327
|
| `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 |
|
package/assets/serve.css
CHANGED
|
@@ -108,16 +108,31 @@ h3 { margin: 0 0 8px; font-size: 13px; line-height: 18px; color: var(--muted); t
|
|
|
108
108
|
.list-head { margin-bottom: 16px; }
|
|
109
109
|
.list-head .meta, .meta { color: var(--muted); }
|
|
110
110
|
.meta { margin: 0; font-size: 12px; line-height: 16px; }
|
|
111
|
-
.jobs { display: flex; flex-direction: column;
|
|
111
|
+
.jobs { display: flex; flex-direction: column; gap: 10px; }
|
|
112
112
|
.job {
|
|
113
|
-
display:
|
|
114
|
-
|
|
115
|
-
gap:
|
|
116
|
-
padding: 12px
|
|
117
|
-
|
|
113
|
+
display: flex;
|
|
114
|
+
flex-direction: column;
|
|
115
|
+
gap: 8px;
|
|
116
|
+
padding: 14px 16px 12px;
|
|
117
|
+
background: var(--card);
|
|
118
|
+
border: 1px solid var(--line);
|
|
119
|
+
border-radius: 10px;
|
|
120
|
+
}
|
|
121
|
+
.job.human { box-shadow: inset 3px 0 0 var(--human); }
|
|
122
|
+
.job.retrying { box-shadow: inset 3px 0 0 var(--changes); }
|
|
123
|
+
.job .d-actions {
|
|
124
|
+
margin: 4px -16px -12px;
|
|
125
|
+
padding: 10px 16px;
|
|
126
|
+
border-top: 1px solid var(--line);
|
|
127
|
+
background: color-mix(in srgb, var(--bg) 55%, transparent);
|
|
128
|
+
border-radius: 0 0 10px 10px;
|
|
129
|
+
}
|
|
130
|
+
.job.decision .approve button {
|
|
131
|
+
background: var(--primary);
|
|
132
|
+
color: white;
|
|
133
|
+
border-color: transparent;
|
|
134
|
+
font-weight: 500;
|
|
118
135
|
}
|
|
119
|
-
.job.human { box-shadow: inset 2px 0 0 var(--human); }
|
|
120
|
-
.job.retrying { box-shadow: inset 2px 0 0 var(--changes); }
|
|
121
136
|
.job-top { display: flex; align-items: baseline; gap: 8px; }
|
|
122
137
|
.job-title { color: var(--ink); font-size: 15px; line-height: 22px; }
|
|
123
138
|
.job-title:hover { color: var(--code); }
|
|
@@ -140,8 +155,116 @@ h3 { margin: 0 0 8px; font-size: 13px; line-height: 18px; color: var(--muted); t
|
|
|
140
155
|
}
|
|
141
156
|
.links { margin: 6px 0 0; display: flex; gap: 10px; }
|
|
142
157
|
.deps { margin: 6px 0 0; }
|
|
143
|
-
.
|
|
144
|
-
.
|
|
158
|
+
.ask { margin: 8px 0 0; font-size: 15px; line-height: 22px; }
|
|
159
|
+
.ran { margin: 4px 0 0; color: var(--muted); font-size: 12px; line-height: 16px; }
|
|
160
|
+
.ticks { display: flex; flex-wrap: wrap; gap: 6px; list-style: none; margin: 10px 0 0; padding: 0; }
|
|
161
|
+
.tick {
|
|
162
|
+
display: inline-flex; align-items: center; gap: 4px;
|
|
163
|
+
padding: 2px 8px; border-radius: 999px; background: var(--code-bg);
|
|
164
|
+
font: 11px/16px var(--mono); color: var(--faint);
|
|
165
|
+
}
|
|
166
|
+
.tick.done { color: var(--pass); }
|
|
167
|
+
.tick.running { color: var(--live); }
|
|
168
|
+
.tick.failed { color: var(--human); }
|
|
169
|
+
.mark { font-size: 11px; }
|
|
170
|
+
.chip {
|
|
171
|
+
display: inline-flex; align-items: center; height: 18px; padding: 0 7px;
|
|
172
|
+
border-radius: 999px; background: var(--code-bg); color: var(--muted);
|
|
173
|
+
font: 11px/18px var(--mono);
|
|
174
|
+
}
|
|
175
|
+
.chip.running, .chip.ok { color: var(--live); }
|
|
176
|
+
.chip.needs-you { color: var(--human); }
|
|
177
|
+
.chip.needs-hand, .chip.failed { color: var(--changes); }
|
|
178
|
+
.chip.done { color: var(--pass); }
|
|
179
|
+
.chip.queued { color: var(--faint); }
|
|
180
|
+
.prose { margin-top: 10px; }
|
|
181
|
+
.prose h1, .prose h2, .prose h3 {
|
|
182
|
+
margin: 12px 0 6px; color: var(--ink); font-size: 15px; line-height: 22px;
|
|
183
|
+
text-transform: none; letter-spacing: 0;
|
|
184
|
+
}
|
|
185
|
+
.prose p { margin: 8px 0; }
|
|
186
|
+
.prose ul, .prose ol { margin: 8px 0; padding-left: 1.2rem; }
|
|
187
|
+
.prose li { margin: 2px 0; }
|
|
188
|
+
.prose pre {
|
|
189
|
+
margin: 8px 0; padding: 8px 10px; overflow: auto; border-radius: 6px; background: var(--bg);
|
|
190
|
+
}
|
|
191
|
+
.prose code { font-family: var(--mono); font-size: 12px; }
|
|
192
|
+
.prose a { color: var(--code); }
|
|
193
|
+
.findings { margin-top: 12px; }
|
|
194
|
+
.finding-group { margin: 0 0 10px; }
|
|
195
|
+
.finding-group h3 { margin: 0 0 4px; }
|
|
196
|
+
.finding-group ul { list-style: none; margin: 0; padding: 0; }
|
|
197
|
+
.finding-group li {
|
|
198
|
+
display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 8px;
|
|
199
|
+
padding: 6px 0; border-top: 1px solid var(--line); font-size: 13px; line-height: 18px;
|
|
200
|
+
}
|
|
201
|
+
.finding-group code { color: var(--code); }
|
|
202
|
+
.notes { margin: 8px 0 0; padding-left: 18px; }
|
|
203
|
+
.pr-line { margin: 8px 0 0; }
|
|
204
|
+
.raw, .log { margin-top: 10px; }
|
|
205
|
+
.raw summary, .log summary { cursor: pointer; color: var(--muted); font-size: 12px; }
|
|
206
|
+
.head-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
|
207
|
+
#compact {
|
|
208
|
+
width: 32px; height: 32px; border-radius: 8px; border: 1px solid var(--line-strong);
|
|
209
|
+
background: var(--card); color: var(--ink); cursor: pointer;
|
|
210
|
+
}
|
|
211
|
+
#compact svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 1.8; }
|
|
212
|
+
.filters { display: flex; flex-wrap: wrap; gap: 16px; margin: 0 0 14px; }
|
|
213
|
+
.filter { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
|
|
214
|
+
.repo-f, .wf-f {
|
|
215
|
+
height: 28px; padding: 0 12px; border: 1px solid var(--line-strong); border-radius: 999px;
|
|
216
|
+
background: var(--card); color: var(--muted); font: 12px/16px var(--mono); cursor: pointer;
|
|
217
|
+
}
|
|
218
|
+
.repo-f[aria-pressed="true"], .wf-f[aria-pressed="true"] {
|
|
219
|
+
border-color: transparent; background: color-mix(in srgb, var(--merged) 28%, var(--card)); color: var(--ink);
|
|
220
|
+
}
|
|
221
|
+
.repo-f:hover, .wf-f:hover { color: var(--ink); }
|
|
222
|
+
.board-scroll { overflow-x: auto; padding-bottom: 8px; }
|
|
223
|
+
.board {
|
|
224
|
+
display: grid;
|
|
225
|
+
grid-template-columns: var(--cols, repeat(5, minmax(180px, 1fr)));
|
|
226
|
+
gap: 12px;
|
|
227
|
+
align-items: start;
|
|
228
|
+
min-width: 920px;
|
|
229
|
+
}
|
|
230
|
+
.col {
|
|
231
|
+
background: var(--panel);
|
|
232
|
+
border: 1px solid var(--line);
|
|
233
|
+
border-radius: 10px;
|
|
234
|
+
padding: 10px;
|
|
235
|
+
min-height: 140px;
|
|
236
|
+
}
|
|
237
|
+
.col .full { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
|
|
238
|
+
.col h2 { font-size: 13px; line-height: 18px; color: var(--muted); text-transform: uppercase; letter-spacing: .04em; }
|
|
239
|
+
.col .count { font: 11px/16px var(--mono); color: var(--faint); }
|
|
240
|
+
.slim-face { display: none; }
|
|
241
|
+
.col.slim { min-height: 160px; padding: 8px; }
|
|
242
|
+
.col.slim .full, .col.slim .card, .col.slim .empty { display: none; }
|
|
243
|
+
.col.slim .slim-face {
|
|
244
|
+
display: flex; align-items: center; gap: 8px; writing-mode: vertical-rl; transform: rotate(180deg);
|
|
245
|
+
color: var(--muted); font-size: 12px; letter-spacing: .04em; text-transform: uppercase;
|
|
246
|
+
}
|
|
247
|
+
.card {
|
|
248
|
+
display: block;
|
|
249
|
+
margin-top: 8px;
|
|
250
|
+
padding: 10px 12px;
|
|
251
|
+
background: var(--card);
|
|
252
|
+
border: 1px solid var(--line);
|
|
253
|
+
border-radius: 8px;
|
|
254
|
+
}
|
|
255
|
+
.card .job-title { display: block; color: var(--ink); font-size: 14px; line-height: 20px; }
|
|
256
|
+
.card .job-meta { margin-top: 6px; }
|
|
257
|
+
.card:hover { background: var(--card-hover); }
|
|
258
|
+
.state-group { margin: 0 0 18px; }
|
|
259
|
+
.state-group h3 { display: flex; align-items: center; gap: 8px; }
|
|
260
|
+
.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin: 0 0 18px; }
|
|
261
|
+
.stat-grid .stat {
|
|
262
|
+
margin: 0; padding: 14px; background: var(--card); border: 1px solid var(--line); border-radius: 10px;
|
|
263
|
+
}
|
|
264
|
+
.stat-num { margin: 0; font-size: 28px; line-height: 34px; font-weight: 500; }
|
|
265
|
+
.run-group { margin: 0 0 18px; }
|
|
266
|
+
.run-group h3 { color: var(--ink); text-transform: none; letter-spacing: 0; font-size: 13px; }
|
|
267
|
+
.phase .text, .timeline .text, .changes .text, .raw .text, .log .text { max-height: 220px; overflow: auto; }
|
|
145
268
|
.feedback { margin: 6px 0 0; padding-left: 18px; }
|
|
146
269
|
.repo-block { margin-bottom: 24px; }
|
|
147
270
|
.repo-block h3 { margin-bottom: 0; }
|
|
@@ -157,7 +280,8 @@ h3 { margin: 0 0 8px; font-size: 13px; line-height: 18px; color: var(--muted); t
|
|
|
157
280
|
padding: 0 0 16px 14px;
|
|
158
281
|
margin-left: 6px;
|
|
159
282
|
}
|
|
160
|
-
.phase.completed { border-color: var(--pass); }
|
|
283
|
+
.phase.done, .phase.completed { border-color: var(--pass); }
|
|
284
|
+
.phase.running { border-color: var(--live); }
|
|
161
285
|
.phase.failed { border-color: var(--human); }
|
|
162
286
|
.phase header, .timeline li { display: flex; flex-wrap: wrap; gap: 8px; align-items: baseline; }
|
|
163
287
|
.step { font-weight: 500; }
|
|
@@ -285,4 +409,5 @@ body.open .drawer { transform: none; }
|
|
|
285
409
|
.rail { position: static; }
|
|
286
410
|
.d-actions .revise input { min-width: 0; width: 100%; }
|
|
287
411
|
.job { grid-template-columns: 1fr; }
|
|
412
|
+
.board { min-width: 720px; }
|
|
288
413
|
}
|
package/assets/serve.js
CHANGED
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
try { compact = localStorage.getItem("kiln-compact") !== "off"; } catch (err) { compact = true; }
|
|
25
25
|
var repo = "";
|
|
26
26
|
try { repo = localStorage.getItem("kiln-repo") || ""; } catch (err) { repo = ""; }
|
|
27
|
+
var workflow = "";
|
|
28
|
+
try { workflow = localStorage.getItem("kiln-workflow") || ""; } catch (err) { workflow = ""; }
|
|
27
29
|
|
|
28
30
|
var COMPACT = {
|
|
29
31
|
on: ["expand empty columns", '<path d="M3 12h7m0 0-3-3m3 3-3 3M21 12h-7m0 0 3-3m-3 3 3 3M12 4v16"/>'],
|
|
@@ -66,12 +68,19 @@
|
|
|
66
68
|
var cards = document.querySelectorAll(".board .card");
|
|
67
69
|
for (var i = 0; i < cards.length; i++) {
|
|
68
70
|
var cardRepo = cards[i].getAttribute("data-repo") || "";
|
|
69
|
-
cards[i].
|
|
71
|
+
var cardWorkflow = cards[i].getAttribute("data-workflow") || "";
|
|
72
|
+
var hideRepo = Boolean(repo) && cardRepo !== repo;
|
|
73
|
+
var hideWorkflow = Boolean(workflow) && cardWorkflow !== workflow;
|
|
74
|
+
cards[i].hidden = hideRepo || hideWorkflow;
|
|
70
75
|
}
|
|
71
76
|
var chips = document.querySelectorAll(".repo-f");
|
|
72
77
|
for (var j = 0; j < chips.length; j++) {
|
|
73
78
|
chips[j].setAttribute("aria-pressed", (chips[j].getAttribute("data-repo") || "") === repo ? "true" : "false");
|
|
74
79
|
}
|
|
80
|
+
var flows = document.querySelectorAll(".wf-f");
|
|
81
|
+
for (var k = 0; k < flows.length; k++) {
|
|
82
|
+
flows[k].setAttribute("aria-pressed", (flows[k].getAttribute("data-workflow") || "") === workflow ? "true" : "false");
|
|
83
|
+
}
|
|
75
84
|
layout();
|
|
76
85
|
}
|
|
77
86
|
|
|
@@ -185,13 +194,33 @@
|
|
|
185
194
|
view.innerHTML = text ? markdown(localizeIso(text)) : '<p class="empty">Nothing written yet.</p>';
|
|
186
195
|
}
|
|
187
196
|
|
|
188
|
-
function
|
|
197
|
+
function relativeTime(iso) {
|
|
189
198
|
var date = new Date(iso);
|
|
190
199
|
if (!isFinite(date.getTime())) return iso;
|
|
191
|
-
|
|
200
|
+
var delta = Date.now() - date.getTime();
|
|
201
|
+
var future = delta < 0;
|
|
202
|
+
var abs = Math.abs(delta);
|
|
203
|
+
var sec = Math.round(abs / 1000);
|
|
204
|
+
if (sec < 45) return future ? "in a moment" : "just now";
|
|
205
|
+
var min = Math.round(sec / 60);
|
|
206
|
+
var prefix = future ? "in " : "";
|
|
207
|
+
var suffix = future ? "" : " ago";
|
|
208
|
+
if (min < 60) return prefix + min + "m" + suffix;
|
|
209
|
+
var hr = Math.round(min / 60);
|
|
210
|
+
if (hr < 36) return prefix + hr + "h" + suffix;
|
|
211
|
+
var day = Math.round(hr / 24);
|
|
212
|
+
if (day < 14) return prefix + day + "d" + suffix;
|
|
192
213
|
return date.toLocaleString();
|
|
193
214
|
}
|
|
194
215
|
|
|
216
|
+
function localTime(iso, mode) {
|
|
217
|
+
var date = new Date(iso);
|
|
218
|
+
if (!isFinite(date.getTime())) return iso;
|
|
219
|
+
if (mode === "clock" || mode === false) return date.toLocaleTimeString();
|
|
220
|
+
if (mode === "rel") return relativeTime(iso);
|
|
221
|
+
return date.toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" });
|
|
222
|
+
}
|
|
223
|
+
|
|
195
224
|
function localizeIso(text) {
|
|
196
225
|
return String(text).replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})/g, function (iso) {
|
|
197
226
|
return localTime(iso, true);
|
|
@@ -202,7 +231,7 @@
|
|
|
202
231
|
var nodes = (root || document).querySelectorAll("time[datetime]");
|
|
203
232
|
for (var i = 0; i < nodes.length; i++) {
|
|
204
233
|
var iso = nodes[i].getAttribute("datetime");
|
|
205
|
-
if (iso) nodes[i].textContent = localTime(iso, nodes[i].getAttribute("data-time")
|
|
234
|
+
if (iso) nodes[i].textContent = localTime(iso, nodes[i].getAttribute("data-time") || "local");
|
|
206
235
|
}
|
|
207
236
|
}
|
|
208
237
|
|
|
@@ -1185,6 +1214,16 @@
|
|
|
1185
1214
|
applyRepo();
|
|
1186
1215
|
return;
|
|
1187
1216
|
}
|
|
1217
|
+
var flow = target.closest(".wf-f");
|
|
1218
|
+
if (flow) {
|
|
1219
|
+
workflow = flow.getAttribute("data-workflow") || "";
|
|
1220
|
+
try {
|
|
1221
|
+
if (workflow) localStorage.setItem("kiln-workflow", workflow);
|
|
1222
|
+
else localStorage.removeItem("kiln-workflow");
|
|
1223
|
+
} catch (err) { /* private mode */ }
|
|
1224
|
+
applyRepo();
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1188
1227
|
if (target.closest("a[href]")) return;
|
|
1189
1228
|
if (target.closest("[data-close]")) {
|
|
1190
1229
|
closeDrawer();
|
|
@@ -1215,6 +1254,7 @@
|
|
|
1215
1254
|
});
|
|
1216
1255
|
|
|
1217
1256
|
function stamp(ok) {
|
|
1257
|
+
localizeTimes(document);
|
|
1218
1258
|
var el = document.getElementById("status");
|
|
1219
1259
|
if (!el) return;
|
|
1220
1260
|
if (!ok) {
|
|
@@ -1225,7 +1265,6 @@
|
|
|
1225
1265
|
el.className = "status";
|
|
1226
1266
|
var jobs = document.querySelectorAll(".job").length;
|
|
1227
1267
|
el.textContent = jobs + (jobs === 1 ? " job · " : " jobs · ") + new Date().toLocaleTimeString();
|
|
1228
|
-
localizeTimes(document);
|
|
1229
1268
|
}
|
|
1230
1269
|
|
|
1231
1270
|
function poll() {
|
package/dist/attention.js
CHANGED
|
@@ -15,7 +15,7 @@ import { SANDBOX_PREFLIGHT_PREFIX } from "./sandbox.js";
|
|
|
15
15
|
*/
|
|
16
16
|
const NEEDS_YOU = ["needs-human", "dead", "failed", "in-review", "invalid"];
|
|
17
17
|
const IN_FLIGHT = ["running", "approved", "queued", "blocked"];
|
|
18
|
-
const SETTLED = ["done", "rejected"];
|
|
18
|
+
const SETTLED = ["done", "rejected", "cancelled"];
|
|
19
19
|
/** Reasons an automatic retry is allowed to schedule. A rejected gate is not one of these. */
|
|
20
20
|
const RETRYABLE_REASONS = new Set(["stalled", "crashed", "orphaned", "failed"]);
|
|
21
21
|
/**
|
|
@@ -102,6 +102,8 @@ function toItem(job) {
|
|
|
102
102
|
export function attentionReason(job, tail = errorTail(job)) {
|
|
103
103
|
switch (job.state) {
|
|
104
104
|
case "needs-human": {
|
|
105
|
+
if (job.reviewPostError)
|
|
106
|
+
return job.reviewPostError;
|
|
105
107
|
if (job.stopReason === "attempt-cap")
|
|
106
108
|
return "Attempt cap reached";
|
|
107
109
|
if (job.stopReason === "fingerprint")
|
|
@@ -147,6 +149,8 @@ export function attentionReason(job, tail = errorTail(job)) {
|
|
|
147
149
|
return job.prUrl ? "Merged" : "Done";
|
|
148
150
|
case "rejected":
|
|
149
151
|
return "Rejected";
|
|
152
|
+
case "cancelled":
|
|
153
|
+
return "Cancelled";
|
|
150
154
|
default:
|
|
151
155
|
return job.state;
|
|
152
156
|
}
|
package/dist/board-view.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import {
|
|
3
|
-
import { feedbackRoundLabel } from "./feedback.js";
|
|
2
|
+
import { isFinished, isHand, isInbox, projectJob } from "./job-view.js";
|
|
4
3
|
import { ledgerRows } from "./ledger.js";
|
|
5
4
|
import { localDay, spendSnapshot } from "./rails.js";
|
|
5
|
+
export { isFinished, isHand, isInbox };
|
|
6
6
|
/**
|
|
7
7
|
* Lists, stats, and job pages for the serve shell. Job state still comes
|
|
8
8
|
* from `jobState` (via the status view). Everything else is read off the
|
|
@@ -60,25 +60,8 @@ export function parseSeenQuery(values) {
|
|
|
60
60
|
}
|
|
61
61
|
return seen;
|
|
62
62
|
}
|
|
63
|
-
export function isInbox(job) {
|
|
64
|
-
if (job.canApprove || job.canReject || job.canRevise)
|
|
65
|
-
return true;
|
|
66
|
-
return job.state === "needs-human" && job.gates.length > 0 && !job.stopReason && !job.feedbackReason;
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Needs a person, and not an open gate. A failure that still has a scheduled
|
|
70
|
-
* retry is in flight, so it stays off this list.
|
|
71
|
-
*/
|
|
72
|
-
export function isHand(job) {
|
|
73
|
-
if (isInbox(job) || scheduledRetry(job))
|
|
74
|
-
return false;
|
|
75
|
-
return job.state === "needs-human" || job.state === "failed" || job.state === "dead" || job.state === "invalid";
|
|
76
|
-
}
|
|
77
|
-
export function isFinished(job) {
|
|
78
|
-
return job.state === "done" || job.state === "rejected";
|
|
79
|
-
}
|
|
80
63
|
export function buildBoard(jobs, events) {
|
|
81
|
-
const rows = jobs.map((job) =>
|
|
64
|
+
const rows = jobs.map((job) => projectJob(job, events));
|
|
82
65
|
const inbox = rows.filter((job) => job.inbox);
|
|
83
66
|
const hand = rows.filter((job) => job.hand);
|
|
84
67
|
const history = rows.filter((job) => job.finished);
|
|
@@ -272,7 +255,7 @@ export function jobDetail(input) {
|
|
|
272
255
|
const jobEvents = input.events.filter((event) => event.job_id === input.job.jobId);
|
|
273
256
|
const changes = jobChanges(input.job, input.events, input.commits ?? [], input.diffStat);
|
|
274
257
|
return {
|
|
275
|
-
job:
|
|
258
|
+
job: projectJob(input.job, input.events),
|
|
276
259
|
overview: phaseFeed(input.events, input.job.jobId),
|
|
277
260
|
changes,
|
|
278
261
|
timeline: jobEvents,
|
|
@@ -442,147 +425,6 @@ export function branchOf(job, events) {
|
|
|
442
425
|
}
|
|
443
426
|
return branch;
|
|
444
427
|
}
|
|
445
|
-
function toBoardJob(job, events) {
|
|
446
|
-
const inbox = isInbox(job);
|
|
447
|
-
const hand = isHand(job);
|
|
448
|
-
const finished = isFinished(job);
|
|
449
|
-
const failure = latestFailure(events, job.jobId);
|
|
450
|
-
const activity = jobActivity(events, job.jobId);
|
|
451
|
-
const waiting = scheduledRetry(job);
|
|
452
|
-
const reason = hand ? handLine(job, failure) : attentionReason(job);
|
|
453
|
-
const notes = [];
|
|
454
|
-
if (job.launchError?.trim())
|
|
455
|
-
notes.push(job.launchError.trim());
|
|
456
|
-
if (job.failureDetail?.trim())
|
|
457
|
-
notes.push(job.failureDetail.trim());
|
|
458
|
-
if (job.waitReason)
|
|
459
|
-
notes.push(job.waitReason);
|
|
460
|
-
const feedback = [];
|
|
461
|
-
for (const round of job.feedbackRounds ?? []) {
|
|
462
|
-
const url = round.items.find((item) => item.url)?.url;
|
|
463
|
-
feedback.push({ label: feedbackRoundLabel(round), ...(url ? { url } : {}) });
|
|
464
|
-
}
|
|
465
|
-
let historyLine;
|
|
466
|
-
if (finished)
|
|
467
|
-
historyLine = finishedLine(job);
|
|
468
|
-
return {
|
|
469
|
-
jobId: job.jobId,
|
|
470
|
-
title: job.title || job.jobId,
|
|
471
|
-
repo: job.repo,
|
|
472
|
-
workflow: job.workflow,
|
|
473
|
-
state: job.state,
|
|
474
|
-
reason,
|
|
475
|
-
inbox,
|
|
476
|
-
hand,
|
|
477
|
-
finished,
|
|
478
|
-
retry: (job.state === "failed" || job.state === "dead" || (job.state === "needs-human" && Boolean(job.stopReason))) && job.gates.length === 0,
|
|
479
|
-
sandboxed: job.sandboxed === true,
|
|
480
|
-
canApprove: job.canApprove,
|
|
481
|
-
canReject: job.canReject,
|
|
482
|
-
canRevise: job.canRevise,
|
|
483
|
-
attemptCount: job.attemptCount,
|
|
484
|
-
maxAttempts: job.maxAttempts,
|
|
485
|
-
scheduledRetry: waiting,
|
|
486
|
-
...(waiting && job.nextRetryAt ? { nextRetryAt: job.nextRetryAt } : {}),
|
|
487
|
-
...(attemptLine(job) ? { attemptLine: attemptLine(job) } : {}),
|
|
488
|
-
...(failure?.class ? { failureClass: failure.class } : {}),
|
|
489
|
-
...(failure?.fingerprint ? { fingerprint: shortFingerprint(failure.fingerprint) } : {}),
|
|
490
|
-
...(activity.latest ? { latestEventAt: activity.latest } : {}),
|
|
491
|
-
eventCount: activity.count,
|
|
492
|
-
...(job.prUrl ? { prUrl: job.prUrl } : {}),
|
|
493
|
-
...(job.prNumber ? { prNumber: job.prNumber } : {}),
|
|
494
|
-
dependsOn: job.dependsOn.map((dep) => ({
|
|
495
|
-
jobId: dep.jobId,
|
|
496
|
-
state: dep.state,
|
|
497
|
-
...(dep.blockedDetail ? { detail: dep.blockedDetail } : {}),
|
|
498
|
-
})),
|
|
499
|
-
evidence: evidenceTexts(job),
|
|
500
|
-
feedback,
|
|
501
|
-
notes,
|
|
502
|
-
...(historyLine ? { historyLine } : {}),
|
|
503
|
-
findings: Boolean(job.findingsPath),
|
|
504
|
-
spec: Boolean(job.specPath),
|
|
505
|
-
log: Boolean(job.logPath),
|
|
506
|
-
...(job.logTail?.trim() ? { logTail: job.logTail.trim() } : {}),
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
function finishedLine(job) {
|
|
510
|
-
if (job.state === "rejected")
|
|
511
|
-
return "Rejected";
|
|
512
|
-
if (job.prNumber)
|
|
513
|
-
return `merged · PR #${job.prNumber}`;
|
|
514
|
-
if (job.prUrl && httpUrl(job.prUrl))
|
|
515
|
-
return "merged · PR";
|
|
516
|
-
return job.branch ? `landed locally · ${job.branch}` : "landed locally";
|
|
517
|
-
}
|
|
518
|
-
function httpUrl(url) {
|
|
519
|
-
try {
|
|
520
|
-
const parsed = new URL(url);
|
|
521
|
-
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
522
|
-
}
|
|
523
|
-
catch {
|
|
524
|
-
return false;
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
function handLine(job, failure) {
|
|
528
|
-
let label = "Needs a person";
|
|
529
|
-
if (job.stopReason === "attempt-cap")
|
|
530
|
-
label = "Attempt cap";
|
|
531
|
-
else if (job.stopReason === "fingerprint")
|
|
532
|
-
label = "Same failure";
|
|
533
|
-
else if (job.feedbackReason === "feedback-exhausted")
|
|
534
|
-
label = "Fix round cap";
|
|
535
|
-
else if (job.feedbackReason === "feedback-ambiguous")
|
|
536
|
-
label = "Feedback unclear";
|
|
537
|
-
else if (job.state === "failed" || job.state === "dead" || job.state === "invalid")
|
|
538
|
-
label = attentionReason(job);
|
|
539
|
-
const parts = [label];
|
|
540
|
-
if (failure?.class)
|
|
541
|
-
parts.push(failure.class);
|
|
542
|
-
const fingerprint = shortFingerprint(failure?.fingerprint);
|
|
543
|
-
if (fingerprint)
|
|
544
|
-
parts.push(fingerprint);
|
|
545
|
-
return parts.join(" · ");
|
|
546
|
-
}
|
|
547
|
-
function evidenceTexts(job) {
|
|
548
|
-
const texts = [];
|
|
549
|
-
if (job.workflow !== "investigation" && job.diffSummary?.trim())
|
|
550
|
-
texts.push(job.diffSummary.trim());
|
|
551
|
-
for (const gate of job.gates) {
|
|
552
|
-
const body = gate.rendered?.trim() || gate.prompt?.trim();
|
|
553
|
-
if (body)
|
|
554
|
-
texts.push(body);
|
|
555
|
-
for (const review of gate.reviews ?? [])
|
|
556
|
-
if (review.summary?.trim())
|
|
557
|
-
texts.push(review.summary.trim());
|
|
558
|
-
for (const artifact of gate.artifacts ?? [])
|
|
559
|
-
if (artifact.summary?.trim())
|
|
560
|
-
texts.push(artifact.summary.trim());
|
|
561
|
-
}
|
|
562
|
-
return texts;
|
|
563
|
-
}
|
|
564
|
-
function attemptLine(job) {
|
|
565
|
-
if (job.attemptCount <= 0 && !job.lastFailureReason)
|
|
566
|
-
return undefined;
|
|
567
|
-
const parts = [];
|
|
568
|
-
if (job.attemptCount > 0)
|
|
569
|
-
parts.push(`attempt ${job.attemptCount}/${job.maxAttempts}`);
|
|
570
|
-
if (job.lastFailureReason)
|
|
571
|
-
parts.push(job.lastFailureReason);
|
|
572
|
-
return parts.join(" · ");
|
|
573
|
-
}
|
|
574
|
-
function jobActivity(events, jobId) {
|
|
575
|
-
let count = 0;
|
|
576
|
-
let latest;
|
|
577
|
-
for (const event of events) {
|
|
578
|
-
if (event.job_id !== jobId)
|
|
579
|
-
continue;
|
|
580
|
-
count += 1;
|
|
581
|
-
if (!latest || event.ts > latest)
|
|
582
|
-
latest = event.ts;
|
|
583
|
-
}
|
|
584
|
-
return { count, ...(latest ? { latest } : {}) };
|
|
585
|
-
}
|
|
586
428
|
function latestFailure(events, jobId) {
|
|
587
429
|
let found;
|
|
588
430
|
for (const event of events) {
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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, reviseJob, runStatus, runTick } from "./driver.js";
|
|
4
|
+
import { answerJob, cancelQueuedJobs, retryJob, reviseJob, runStatus, runTick } from "./driver.js";
|
|
5
5
|
import { productionHost } from "./host.js";
|
|
6
6
|
import { pauseFactory, resumeFactory } from "./rails.js";
|
|
7
7
|
import { parseSince, renderLedgerFile } from "./ledger.js";
|
|
@@ -23,6 +23,7 @@ function usage() {
|
|
|
23
23
|
" kiln reject <jobId> [--config <file>]",
|
|
24
24
|
" kiln revise <jobId> --note <text> [--config <file>]",
|
|
25
25
|
" kiln retry <jobId> [--config <file>]",
|
|
26
|
+
" kiln cancel --queued [--repo <name>] [--workflow <name>] [--config <file>]",
|
|
26
27
|
" kiln pause [--config <file>]",
|
|
27
28
|
" kiln resume [--config <file>]",
|
|
28
29
|
" kiln ledger [--since 7d] [--repo <name>] [--config <file>]",
|
|
@@ -63,7 +64,9 @@ function usage() {
|
|
|
63
64
|
"yak/<run-id> and opens a draft pull request. An investigation is done when",
|
|
64
65
|
"its gate is approved. It does not open a pull request. status prints the",
|
|
65
66
|
"derived state, and the findings path once an investigation is approved.",
|
|
66
|
-
"approve and reject answer the human gate.
|
|
67
|
+
"approve and reject answer the human gate. Approving a review draft posts",
|
|
68
|
+
"it as a pull request comment and does not approve or merge that pull request.",
|
|
69
|
+
"retry launches a failed or dead",
|
|
67
70
|
"job again, the same way tick launches, and clears the attempt ledger.",
|
|
68
71
|
"A landed job that only failed to open its pull request is delivered again.",
|
|
69
72
|
"A running job with no yak event or log output for stallTimeoutMs (default",
|
|
@@ -94,6 +97,12 @@ function usage() {
|
|
|
94
97
|
"when a slot was missed. Jira polls are per repo. Kiln-owned pull",
|
|
95
98
|
"request activity uses the same read as the fix loop. A failed fire",
|
|
96
99
|
"waits for the next slot. Pause and the daily cap skip the fire.",
|
|
100
|
+
"review-requests only picks pull requests labeled kiln-review, unless",
|
|
101
|
+
"the trigger sets label (repos.<name>.reviewLabel overrides that repo).",
|
|
102
|
+
"One fire creates at most 3 new review jobs, and at most 5 review jobs",
|
|
103
|
+
"may be queued or running. The rest are skipped until a later fire.",
|
|
104
|
+
"kiln cancel --queued cancels jobs that have not started. It does not",
|
|
105
|
+
"delete branches. A second cancel changes nothing.",
|
|
97
106
|
"",
|
|
98
107
|
"serve listens on 127.0.0.1. The page is a board of workflow stages:",
|
|
99
108
|
"queued, spec, build, review, approve, land, PR review, Done (landed locally),",
|
|
@@ -116,7 +125,7 @@ function usage() {
|
|
|
116
125
|
process.exit(2);
|
|
117
126
|
}
|
|
118
127
|
function parseFlags(argv) {
|
|
119
|
-
const bag = { positionals: [], after: [], dryRun: false, stack: false, noTick: false };
|
|
128
|
+
const bag = { positionals: [], after: [], dryRun: false, stack: false, noTick: false, queued: false };
|
|
120
129
|
for (let i = 0; i < argv.length; i += 1) {
|
|
121
130
|
const flag = argv[i];
|
|
122
131
|
const next = argv[i + 1];
|
|
@@ -205,6 +214,10 @@ function parseFlags(argv) {
|
|
|
205
214
|
bag.stack = true;
|
|
206
215
|
continue;
|
|
207
216
|
}
|
|
217
|
+
if (flag === "--queued") {
|
|
218
|
+
bag.queued = true;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
208
221
|
if (flag === "--deliver") {
|
|
209
222
|
if (next !== "local" && next !== "pr")
|
|
210
223
|
usage();
|
|
@@ -418,6 +431,37 @@ function statusCommand(argv) {
|
|
|
418
431
|
const config = resolveKilnConfig({ configPath: flags.configPath });
|
|
419
432
|
process.stdout.write(runStatus(config, flags.positionals[0]));
|
|
420
433
|
}
|
|
434
|
+
function cancelCommand(argv) {
|
|
435
|
+
const flags = parseFlags(argv);
|
|
436
|
+
rejectServeFlags(flags);
|
|
437
|
+
if (!flags.queued ||
|
|
438
|
+
flags.positionals.length !== 0 ||
|
|
439
|
+
flags.test ||
|
|
440
|
+
flags.spec ||
|
|
441
|
+
flags.prompt ||
|
|
442
|
+
flags.jira ||
|
|
443
|
+
flags.title ||
|
|
444
|
+
flags.issue ||
|
|
445
|
+
flags.context ||
|
|
446
|
+
flags.dryRun ||
|
|
447
|
+
flags.stack ||
|
|
448
|
+
flags.after.length > 0 ||
|
|
449
|
+
flags.deliver ||
|
|
450
|
+
flags.base ||
|
|
451
|
+
flags.note) {
|
|
452
|
+
usage();
|
|
453
|
+
}
|
|
454
|
+
const config = resolveKilnConfig({ configPath: flags.configPath });
|
|
455
|
+
const result = cancelQueuedJobs(config, {
|
|
456
|
+
...(flags.repo ? { repo: flags.repo } : {}),
|
|
457
|
+
...(flags.workflow ? { workflow: flags.workflow } : {}),
|
|
458
|
+
});
|
|
459
|
+
if (result.cancelled.length === 0) {
|
|
460
|
+
process.stdout.write("cancelled 0\n");
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
process.stdout.write(`${result.cancelled.map((id) => `cancelled ${id}`).join("\n")}\n`);
|
|
464
|
+
}
|
|
421
465
|
function retryCommand(argv) {
|
|
422
466
|
const flags = parseFlags(argv);
|
|
423
467
|
rejectServeFlags(flags);
|
|
@@ -562,6 +606,10 @@ async function main() {
|
|
|
562
606
|
retryCommand(argv.slice(1));
|
|
563
607
|
return;
|
|
564
608
|
}
|
|
609
|
+
if (command === "cancel") {
|
|
610
|
+
cancelCommand(argv.slice(1));
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
565
613
|
if (command === "revise") {
|
|
566
614
|
reviseCommand(argv.slice(1));
|
|
567
615
|
return;
|
package/dist/config.js
CHANGED
|
@@ -33,6 +33,7 @@ export function loadKilnConfig(configPath, options = {}) {
|
|
|
33
33
|
...(entry.branch ? { branchTemplate: entry.branch } : {}),
|
|
34
34
|
...(entry.maxAttempts !== undefined ? { maxAttempts: entry.maxAttempts } : {}),
|
|
35
35
|
...(entry.maxFixRounds !== undefined ? { maxFixRounds: entry.maxFixRounds } : {}),
|
|
36
|
+
...(entry.reviewLabel ? { reviewLabel: entry.reviewLabel } : {}),
|
|
36
37
|
};
|
|
37
38
|
}
|
|
38
39
|
const kilnDir = configDir;
|