@lchase/kiln 0.13.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 +3 -1
- package/dist/board-view.js +4 -169
- package/dist/cli.js +48 -2
- package/dist/config.js +1 -0
- package/dist/driver.js +44 -9
- package/dist/events.js +10 -1
- package/dist/host.js +45 -2
- 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/schemas.js +21 -1
- package/dist/serve-page.js +275 -66
- package/dist/serve.js +46 -2
- package/dist/settings.js +4 -2
- package/dist/triggers.js +220 -21
- package/package.json +1 -1
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 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`.
|
|
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
|
/**
|
|
@@ -149,6 +149,8 @@ export function attentionReason(job, tail = errorTail(job)) {
|
|
|
149
149
|
return job.prUrl ? "Merged" : "Done";
|
|
150
150
|
case "rejected":
|
|
151
151
|
return "Rejected";
|
|
152
|
+
case "cancelled":
|
|
153
|
+
return "Cancelled";
|
|
152
154
|
default:
|
|
153
155
|
return job.state;
|
|
154
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,27 +60,8 @@ export function parseSeenQuery(values) {
|
|
|
60
60
|
}
|
|
61
61
|
return seen;
|
|
62
62
|
}
|
|
63
|
-
export function isInbox(job) {
|
|
64
|
-
if (job.reviewPostError)
|
|
65
|
-
return false;
|
|
66
|
-
if (job.canApprove || job.canReject || job.canRevise)
|
|
67
|
-
return true;
|
|
68
|
-
return job.state === "needs-human" && job.gates.length > 0 && !job.stopReason && !job.feedbackReason;
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Needs a person, and not an open gate. A failure that still has a scheduled
|
|
72
|
-
* retry is in flight, so it stays off this list.
|
|
73
|
-
*/
|
|
74
|
-
export function isHand(job) {
|
|
75
|
-
if (isInbox(job) || scheduledRetry(job))
|
|
76
|
-
return false;
|
|
77
|
-
return job.state === "needs-human" || job.state === "failed" || job.state === "dead" || job.state === "invalid";
|
|
78
|
-
}
|
|
79
|
-
export function isFinished(job) {
|
|
80
|
-
return job.state === "done" || job.state === "rejected";
|
|
81
|
-
}
|
|
82
63
|
export function buildBoard(jobs, events) {
|
|
83
|
-
const rows = jobs.map((job) =>
|
|
64
|
+
const rows = jobs.map((job) => projectJob(job, events));
|
|
84
65
|
const inbox = rows.filter((job) => job.inbox);
|
|
85
66
|
const hand = rows.filter((job) => job.hand);
|
|
86
67
|
const history = rows.filter((job) => job.finished);
|
|
@@ -274,7 +255,7 @@ export function jobDetail(input) {
|
|
|
274
255
|
const jobEvents = input.events.filter((event) => event.job_id === input.job.jobId);
|
|
275
256
|
const changes = jobChanges(input.job, input.events, input.commits ?? [], input.diffStat);
|
|
276
257
|
return {
|
|
277
|
-
job:
|
|
258
|
+
job: projectJob(input.job, input.events),
|
|
278
259
|
overview: phaseFeed(input.events, input.job.jobId),
|
|
279
260
|
changes,
|
|
280
261
|
timeline: jobEvents,
|
|
@@ -444,152 +425,6 @@ export function branchOf(job, events) {
|
|
|
444
425
|
}
|
|
445
426
|
return branch;
|
|
446
427
|
}
|
|
447
|
-
function toBoardJob(job, events) {
|
|
448
|
-
const inbox = isInbox(job);
|
|
449
|
-
const hand = isHand(job);
|
|
450
|
-
const finished = isFinished(job);
|
|
451
|
-
const failure = latestFailure(events, job.jobId);
|
|
452
|
-
const activity = jobActivity(events, job.jobId);
|
|
453
|
-
const waiting = scheduledRetry(job);
|
|
454
|
-
const reason = hand ? handLine(job, failure) : attentionReason(job);
|
|
455
|
-
const notes = [];
|
|
456
|
-
if (job.launchError?.trim())
|
|
457
|
-
notes.push(job.launchError.trim());
|
|
458
|
-
if (job.failureDetail?.trim())
|
|
459
|
-
notes.push(job.failureDetail.trim());
|
|
460
|
-
if (job.waitReason)
|
|
461
|
-
notes.push(job.waitReason);
|
|
462
|
-
const feedback = [];
|
|
463
|
-
for (const round of job.feedbackRounds ?? []) {
|
|
464
|
-
const url = round.items.find((item) => item.url)?.url;
|
|
465
|
-
feedback.push({ label: feedbackRoundLabel(round), ...(url ? { url } : {}) });
|
|
466
|
-
}
|
|
467
|
-
let historyLine;
|
|
468
|
-
if (finished)
|
|
469
|
-
historyLine = finishedLine(job);
|
|
470
|
-
return {
|
|
471
|
-
jobId: job.jobId,
|
|
472
|
-
title: job.title || job.jobId,
|
|
473
|
-
repo: job.repo,
|
|
474
|
-
workflow: job.workflow,
|
|
475
|
-
state: job.state,
|
|
476
|
-
reason,
|
|
477
|
-
inbox,
|
|
478
|
-
hand,
|
|
479
|
-
finished,
|
|
480
|
-
retry: (job.state === "failed" || job.state === "dead" || (job.state === "needs-human" && Boolean(job.stopReason))) && job.gates.length === 0,
|
|
481
|
-
sandboxed: job.sandboxed === true,
|
|
482
|
-
canApprove: job.canApprove,
|
|
483
|
-
canReject: job.canReject,
|
|
484
|
-
canRevise: job.canRevise,
|
|
485
|
-
attemptCount: job.attemptCount,
|
|
486
|
-
maxAttempts: job.maxAttempts,
|
|
487
|
-
scheduledRetry: waiting,
|
|
488
|
-
...(waiting && job.nextRetryAt ? { nextRetryAt: job.nextRetryAt } : {}),
|
|
489
|
-
...(attemptLine(job) ? { attemptLine: attemptLine(job) } : {}),
|
|
490
|
-
...(failure?.class ? { failureClass: failure.class } : {}),
|
|
491
|
-
...(failure?.fingerprint ? { fingerprint: shortFingerprint(failure.fingerprint) } : {}),
|
|
492
|
-
...(activity.latest ? { latestEventAt: activity.latest } : {}),
|
|
493
|
-
eventCount: activity.count,
|
|
494
|
-
...(job.prUrl ? { prUrl: job.prUrl } : {}),
|
|
495
|
-
...(job.prNumber ? { prNumber: job.prNumber } : {}),
|
|
496
|
-
dependsOn: job.dependsOn.map((dep) => ({
|
|
497
|
-
jobId: dep.jobId,
|
|
498
|
-
state: dep.state,
|
|
499
|
-
...(dep.blockedDetail ? { detail: dep.blockedDetail } : {}),
|
|
500
|
-
})),
|
|
501
|
-
evidence: evidenceTexts(job),
|
|
502
|
-
feedback,
|
|
503
|
-
notes,
|
|
504
|
-
...(historyLine ? { historyLine } : {}),
|
|
505
|
-
findings: Boolean(job.findingsPath),
|
|
506
|
-
spec: Boolean(job.specPath),
|
|
507
|
-
log: Boolean(job.logPath),
|
|
508
|
-
...(job.logTail?.trim() ? { logTail: job.logTail.trim() } : {}),
|
|
509
|
-
...(job.reviewPostError ? { reviewPostError: job.reviewPostError } : {}),
|
|
510
|
-
};
|
|
511
|
-
}
|
|
512
|
-
function finishedLine(job) {
|
|
513
|
-
if (job.state === "rejected")
|
|
514
|
-
return "Rejected";
|
|
515
|
-
if (job.workflow === "review")
|
|
516
|
-
return job.reviewUrl ? "comment posted" : "approved";
|
|
517
|
-
if (job.prNumber)
|
|
518
|
-
return `merged · PR #${job.prNumber}`;
|
|
519
|
-
if (job.prUrl && httpUrl(job.prUrl))
|
|
520
|
-
return "merged · PR";
|
|
521
|
-
return job.branch ? `landed locally · ${job.branch}` : "landed locally";
|
|
522
|
-
}
|
|
523
|
-
function httpUrl(url) {
|
|
524
|
-
try {
|
|
525
|
-
const parsed = new URL(url);
|
|
526
|
-
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
527
|
-
}
|
|
528
|
-
catch {
|
|
529
|
-
return false;
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
function handLine(job, failure) {
|
|
533
|
-
if (job.reviewPostError)
|
|
534
|
-
return job.reviewPostError;
|
|
535
|
-
let label = "Needs a person";
|
|
536
|
-
if (job.stopReason === "attempt-cap")
|
|
537
|
-
label = "Attempt cap";
|
|
538
|
-
else if (job.stopReason === "fingerprint")
|
|
539
|
-
label = "Same failure";
|
|
540
|
-
else if (job.feedbackReason === "feedback-exhausted")
|
|
541
|
-
label = "Fix round cap";
|
|
542
|
-
else if (job.feedbackReason === "feedback-ambiguous")
|
|
543
|
-
label = "Feedback unclear";
|
|
544
|
-
else if (job.state === "failed" || job.state === "dead" || job.state === "invalid")
|
|
545
|
-
label = attentionReason(job);
|
|
546
|
-
const parts = [label];
|
|
547
|
-
if (failure?.class)
|
|
548
|
-
parts.push(failure.class);
|
|
549
|
-
const fingerprint = shortFingerprint(failure?.fingerprint);
|
|
550
|
-
if (fingerprint)
|
|
551
|
-
parts.push(fingerprint);
|
|
552
|
-
return parts.join(" · ");
|
|
553
|
-
}
|
|
554
|
-
function evidenceTexts(job) {
|
|
555
|
-
const texts = [];
|
|
556
|
-
if (job.workflow !== "investigation" && job.diffSummary?.trim())
|
|
557
|
-
texts.push(job.diffSummary.trim());
|
|
558
|
-
for (const gate of job.gates) {
|
|
559
|
-
const body = gate.rendered?.trim() || gate.prompt?.trim();
|
|
560
|
-
if (body)
|
|
561
|
-
texts.push(body);
|
|
562
|
-
for (const review of gate.reviews ?? [])
|
|
563
|
-
if (review.summary?.trim())
|
|
564
|
-
texts.push(review.summary.trim());
|
|
565
|
-
for (const artifact of gate.artifacts ?? [])
|
|
566
|
-
if (artifact.summary?.trim())
|
|
567
|
-
texts.push(artifact.summary.trim());
|
|
568
|
-
}
|
|
569
|
-
return texts;
|
|
570
|
-
}
|
|
571
|
-
function attemptLine(job) {
|
|
572
|
-
if (job.attemptCount <= 0 && !job.lastFailureReason)
|
|
573
|
-
return undefined;
|
|
574
|
-
const parts = [];
|
|
575
|
-
if (job.attemptCount > 0)
|
|
576
|
-
parts.push(`attempt ${job.attemptCount}/${job.maxAttempts}`);
|
|
577
|
-
if (job.lastFailureReason)
|
|
578
|
-
parts.push(job.lastFailureReason);
|
|
579
|
-
return parts.join(" · ");
|
|
580
|
-
}
|
|
581
|
-
function jobActivity(events, jobId) {
|
|
582
|
-
let count = 0;
|
|
583
|
-
let latest;
|
|
584
|
-
for (const event of events) {
|
|
585
|
-
if (event.job_id !== jobId)
|
|
586
|
-
continue;
|
|
587
|
-
count += 1;
|
|
588
|
-
if (!latest || event.ts > latest)
|
|
589
|
-
latest = event.ts;
|
|
590
|
-
}
|
|
591
|
-
return { count, ...(latest ? { latest } : {}) };
|
|
592
|
-
}
|
|
593
428
|
function latestFailure(events, jobId) {
|
|
594
429
|
let found;
|
|
595
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>]",
|
|
@@ -96,6 +97,12 @@ function usage() {
|
|
|
96
97
|
"when a slot was missed. Jira polls are per repo. Kiln-owned pull",
|
|
97
98
|
"request activity uses the same read as the fix loop. A failed fire",
|
|
98
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.",
|
|
99
106
|
"",
|
|
100
107
|
"serve listens on 127.0.0.1. The page is a board of workflow stages:",
|
|
101
108
|
"queued, spec, build, review, approve, land, PR review, Done (landed locally),",
|
|
@@ -118,7 +125,7 @@ function usage() {
|
|
|
118
125
|
process.exit(2);
|
|
119
126
|
}
|
|
120
127
|
function parseFlags(argv) {
|
|
121
|
-
const bag = { positionals: [], after: [], dryRun: false, stack: false, noTick: false };
|
|
128
|
+
const bag = { positionals: [], after: [], dryRun: false, stack: false, noTick: false, queued: false };
|
|
122
129
|
for (let i = 0; i < argv.length; i += 1) {
|
|
123
130
|
const flag = argv[i];
|
|
124
131
|
const next = argv[i + 1];
|
|
@@ -207,6 +214,10 @@ function parseFlags(argv) {
|
|
|
207
214
|
bag.stack = true;
|
|
208
215
|
continue;
|
|
209
216
|
}
|
|
217
|
+
if (flag === "--queued") {
|
|
218
|
+
bag.queued = true;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
210
221
|
if (flag === "--deliver") {
|
|
211
222
|
if (next !== "local" && next !== "pr")
|
|
212
223
|
usage();
|
|
@@ -420,6 +431,37 @@ function statusCommand(argv) {
|
|
|
420
431
|
const config = resolveKilnConfig({ configPath: flags.configPath });
|
|
421
432
|
process.stdout.write(runStatus(config, flags.positionals[0]));
|
|
422
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
|
+
}
|
|
423
465
|
function retryCommand(argv) {
|
|
424
466
|
const flags = parseFlags(argv);
|
|
425
467
|
rejectServeFlags(flags);
|
|
@@ -564,6 +606,10 @@ async function main() {
|
|
|
564
606
|
retryCommand(argv.slice(1));
|
|
565
607
|
return;
|
|
566
608
|
}
|
|
609
|
+
if (command === "cancel") {
|
|
610
|
+
cancelCommand(argv.slice(1));
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
567
613
|
if (command === "revise") {
|
|
568
614
|
reviseCommand(argv.slice(1));
|
|
569
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;
|