@lchase/kiln 0.4.0 → 0.5.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 CHANGED
@@ -68,7 +68,7 @@ npm run kiln -- repo list
68
68
 
69
69
  ## Add a job
70
70
 
71
- A job is one workflow run. The workflow name is a file `workflows/<name>.yaml`. `feature` is the factory loop. `investigation` answers a spike and stops for a person to approve the findings. Bug and security workflows are next. A later `pr-review` workflow is not in this repo yet.
71
+ A job is one workflow run. The workflow name is a file `workflows/<name>.yaml`. `feature` is the factory loop. `investigation` answers a spike and stops for a person to approve the findings. `fix` is the pull request feedback run: the same builder, on the open pull request head, committing onto a new `yak/<run-id>` that Kiln fast-forwards onto the existing branch. Bug and security workflows are next.
72
72
 
73
73
  ```bash
74
74
  npm run kiln -- add feature --repo web --spec spec.md --title "Show a Sunday summary"
@@ -166,7 +166,7 @@ kiln serve --no-tick
166
166
 
167
167
  `kiln serve` listens on `127.0.0.1` only (port 4100 unless `--port` is set). It reads the same derived state as `kiln status` and calls the same approve, reject, and retry functions. Nothing new is stored.
168
168
 
169
- `/` is the operator board, using the factory dashboard's dark charcoal layout. Columns are workflow stages taken from the job's yaml steps and the current yak step. Queued is first: jobs whose derived state is queued or blocked, with the subtitle `waiting · deps` and dimmer text. A blocked card names the jobs it is waiting on. Spec holds a job only while the spec step is the one running. Then Build (`builder · opus`), Review (`sec · ux · ui · code`), Approve (`approver · opus`), Land, PR review, Merged, and Needs human. An investigation adds Explore. Needs human stays at the right and holds open gates, dead runs, failures that are not waiting on an automatic retry, invalid job files, and rejections. A failed job with `nextRetryAt` still set stays in the column of the step it was on (Build, Review, and so on), dimmer, with one line `retrying 7:35 PM · attempt 1/3 · stalled` in the browser's local time. It is not in the needs-you count. When that retry starts, the card is a normal running card again. An empty column is a narrow strip with the name written down it. The header clock and any timestamps in the drawer use the browser's local time; stored values stay ISO UTC. The bottom toolbar switches between that and always-expanded columns; the choice is stored in `localStorage`. Repo chips across the top filter the cards. A card shows the job id, repo, title, branch, pull request, and one row per review round (security, ux, ui, code). A blocked card names what it is waiting on. Clicking a card opens a drawer. Approve, Reject, and Retry sit in the drawer header when the job is gated or failed. The drawer lists the spec, build, decision, each review, and investigation findings, and the selected file is rendered on the right. `/board` is the same page. The document title is `(N) Kiln`, where N is the needs-you count from the attention view (gates, dead runs, failures with no automatic retry left, and open pull requests). A job waiting out its backoff is not included, and it does not fire the desktop notification. The page polls every 3 seconds. After you opt in from the toolbar, a desktop notification fires when that count goes up. Kiln does not record factory quality averages, so the header does not invent round or human-fix numbers.
169
+ `/` is the operator board, using the factory dashboard's dark charcoal layout. Columns are workflow stages taken from the job's yaml steps and the current yak step. Queued is first: jobs whose derived state is queued or blocked, with the subtitle `waiting · deps` and dimmer text. A blocked card names the jobs it is waiting on. Spec holds a job only while the spec step is the one running. Then Build (`builder · opus`), Review (`sec · ux · ui · code`), Approve (`approver · opus`), Land, PR review, Merged, and Needs human. An investigation adds Explore. Needs human stays at the right and holds open gates, dead runs, failures that are not waiting on an automatic retry, invalid job files, and rejections. A failed job with `nextRetryAt` still set stays in the column of the step it was on (Build, Review, and so on), dimmer, with one line `retrying 7:35 PM · attempt 1/3 · stalled` in the browser's local time. It is not in the needs-you count. When that retry starts, the card is a normal running card again. An empty column is a narrow strip with the name written down it. The header clock and any timestamps in the drawer use the browser's local time; stored values stay ISO UTC. The bottom toolbar switches between that and always-expanded columns; the choice is stored in `localStorage`. Repo chips across the top filter the cards. A card shows the job id, repo, title, branch, pull request, and one row per review round (security, ux, ui, code). A card in PR review also shows each feedback round as its own row: `CI failed`, `changes requested`, then `pushed` or `fixed` once that round lands. A fix that is still running is a normal running card. Only `feedback-exhausted` (and feedback Kiln could not classify) moves to Needs human, with your call. The drawer lists each feedback round's checks and comments, with links. A blocked card names what it is waiting on. Clicking a card opens a drawer. Approve, Reject, and Retry sit in the drawer header when the job is gated or failed. The drawer lists the spec, build, decision, each review, and investigation findings, and the selected file is rendered on the right. `/board` is the same page. The document title is `(N) Kiln`, where N is the needs-you count from the attention view (gates, dead runs, failures with no automatic retry left, and open pull requests). A job waiting out its backoff is not included, and it does not fire the desktop notification. The page polls every 3 seconds. After you opt in from the toolbar, a desktop notification fires when that count goes up. Kiln does not record factory quality averages, so the header does not invent round or human-fix numbers.
170
170
 
171
171
  `GET /jobs/<id>/findings` renders the approved findings markdown as text. HTML in that file is escaped. The page is read-only.
172
172
 
@@ -195,7 +195,9 @@ while true; do npm run kiln -- tick; sleep 30; done
195
195
 
196
196
  A job is ready when its dependencies are met and the number of jobs in flight is under `maxConcurrent`. The default is after merged: every job in `dependsOn` is `done`. `--stack` is met when that one upstream has landed. A rejected gate does not unblock the next job. An ok run with no readable approval does not either.
197
197
 
198
- `stallTimeoutMs` (default `900000`, 15 minutes) and `maxAttempts` (default `3`) live in the kiln config. `stallTimeoutMs` of `0` or below turns stall detection off. A running job with no yak journal event and no log output for that long is killed, process tree included, and marked failed with reason `stalled`. A process that is already gone when `kiln tick` or `kiln serve` starts is `orphaned`. An agent or launch crash is `crashed`. A non-zero exit that is not a gate rejection is `failed`. Those four retry on their own after `10s × 2^(n-1)`, capped at 5 minutes. A rejected gate, a cancelled run, and an ok run with no readable approval do not. After `maxAttempts` the job is `dead`. Each attempt is a file under `<kiln-dir>/attempts/<job-id>.json`, so state stays derived. Status prints `attempts`, `reason`, and `next retry` from the same view `readStatus` returns. On the board, a dead card in Needs human shows `attempt N/M` and the reason. A failure that still has a scheduled retry stays in its stage column instead, with `retrying` and the local time.
198
+ `stallTimeoutMs` (default `900000`, 15 minutes), `maxAttempts` (default `3`), and `maxFixRounds` (default `2`) live in the kiln config. `stallTimeoutMs` of `0` or below turns stall detection off. A running job with no yak journal event and no log output for that long is killed, process tree included, and marked failed with reason `stalled`. A process that is already gone when `kiln tick` or `kiln serve` starts is `orphaned`. An agent or launch crash is `crashed`. A non-zero exit that is not a gate rejection is `failed`. Those four retry on their own after `10s × 2^(n-1)`, capped at 5 minutes. A rejected gate, a cancelled run, and an ok run with no readable approval do not. After `maxAttempts` the job is `dead`. Each attempt is a file under `<kiln-dir>/attempts/<job-id>.json`, so state stays derived. Status prints `attempts`, `reason`, and `next retry` from the same view `readStatus` returns. On the board, a dead card in Needs human shows `attempt N/M` and the reason. A failure that still has a scheduled retry stays in its stage column instead, with `retrying` and the local time.
199
+
200
+ While a `deliver=pr` job is `in-review`, tick reads that pull request with `gh`: check runs for the head, reviews, and unresolved review comments. The operator's inline comments and unresolved threads count; a top-level note counts only when it starts with `/kiln`, and that prefix is left out of the fix prompt. Comments from `kiln` or a `[bot]` login are ignored, and a resolved thread does not start a round. Pending checks are ignored. A completed failing check on a head that no round has recorded, or review feedback newer than the last round, starts `workflows/fix.yaml` with `yak run --from` that head. The prompt is the original spec plus the failing check names (with a short log excerpt) and the review comment bodies, including file and line. The agent commits on `yak/<run-id>` and does not push. Kiln fast-forwards the open pull request branch. It does not open a second pull request, force-push, merge, approve, or comment. The same head and the same feedback do not start two runs. Each round is a file under `<kiln-dir>/feedback/<job-id>.json`. A stall or crash during a fix uses the attempt ledger above. After `maxFixRounds` the job is `needs-human` with reason `feedback-exhausted`. Feedback Kiln cannot classify is `needs-human` with reason `feedback-ambiguous`. The person merges the pull request.
199
201
 
200
202
  `npm run kiln -- status` lists jobs across repos. When a job has `source`, status prints `source <kind> <ref>`. For `needs-human` it prints `approve: kiln approve <id>` and `reject: kiln reject <id>`. Run those as `npm run kiln -- approve <id>` and `npm run kiln -- reject <id>`. For `done` and `in-review` it prints `branch yak/<run-id>`. For `deliver=pr` it also prints the pull request url once one exists. For an investigation whose findings file exists, it prints `findings <path>`. That path is `findingsPath` on the same status view `kiln serve` renders, and the console links it. For `blocked` on a dependency id that has no job file, it prints `blocked on missing job <id>`. While an upstream pull request is open, it prints `waiting for PR #N to merge`. `npm run kiln -- retry <id>` relaunches a failed or dead job, or delivers again when the only failure is opening the pull request, and clears the attempt ledger.
201
203
 
@@ -206,7 +208,7 @@ State is derived from the yak journal, the gate files, and (for `deliver=pr`) `g
206
208
  | `queued` | Dependencies are met, and the job has not started |
207
209
  | `blocked` | An after-merged dependency is not `done`, or a stacked dependency has not landed. A missing job id is named |
208
210
  | `running` | Yak is alive, or the launch process is alive and has not written a journal |
209
- | `needs-human` | The run is suspended on an open gate |
211
+ | `needs-human` | The run is suspended on an open gate, or pull request feedback is exhausted (`feedback-exhausted`) or ambiguous (`feedback-ambiguous`) |
210
212
  | `approved` | The gate says approve and the run has not finished. This still holds a slot |
211
213
  | `rejected` | The run finished ok and the gate says reject |
212
214
  | `failed` | The run failed, the launch died before a journal, the run finished ok with no readable approval, a pull request could not be opened, or a transient failure is waiting to retry |
@@ -265,4 +267,4 @@ npm run kiln -- import ./tickets --repo web --stack
265
267
 
266
268
  ## Not included
267
269
 
268
- No requirement refinement. Bug, security, and pr-review workflows are not in this repo yet. Kiln does not merge pull requests and does not push the base branch.
270
+ No requirement refinement. Bug and security workflows are not in this repo yet. Kiln does not merge, approve, or comment on pull requests, and it does not push the base branch.
package/dist/attention.js CHANGED
@@ -97,6 +97,8 @@ function toItem(job) {
97
97
  export function attentionReason(job, tail = errorTail(job)) {
98
98
  switch (job.state) {
99
99
  case "needs-human": {
100
+ if (job.feedbackReason)
101
+ return job.feedbackReason;
100
102
  const question = gateQuestion(job);
101
103
  return question ? `Gate: ${oneLine(question)}` : "Waiting on a person";
102
104
  }
@@ -225,6 +227,7 @@ function attentionRevision(needsYou, inFlight, doneRecently, graph) {
225
227
  item.job.attemptCount,
226
228
  item.job.lastFailureReason ?? "",
227
229
  item.job.nextRetryAt ?? "",
230
+ feedbackStamp(item.job),
228
231
  ]),
229
232
  inFlight: inFlight.map((item) => [
230
233
  item.job.jobId,
@@ -234,12 +237,19 @@ function attentionRevision(needsYou, inFlight, doneRecently, graph) {
234
237
  item.job.attemptCount,
235
238
  item.job.lastFailureReason ?? "",
236
239
  item.job.nextRetryAt ?? "",
240
+ feedbackStamp(item.job),
237
241
  ]),
238
242
  doneRecently: doneRecently.map((item) => [item.job.jobId, item.job.state]),
239
243
  edges: graph.edges.map((edge) => [edge.from, edge.to, edge.stack]),
240
244
  });
241
245
  return createHash("sha256").update(body).digest("hex").slice(0, 16);
242
246
  }
247
+ function feedbackStamp(job) {
248
+ const rounds = (job.feedbackRounds ?? [])
249
+ .map((round) => `${round.round}:${round.trigger}:${round.outcome ?? ""}:${round.headSha}`)
250
+ .join("|");
251
+ return `${job.feedbackReason ?? ""}:${rounds}`;
252
+ }
243
253
  function oneLine(text, max = 180) {
244
254
  const flat = text.replace(/\s+/g, " ").trim();
245
255
  if (flat.length <= max)
package/dist/cli.js CHANGED
@@ -59,6 +59,10 @@ function usage() {
59
59
  "after 10s, 20s, 40s, and so on, capped at 5 minutes, up to maxAttempts",
60
60
  "(default 3). The job is then dead. A rejected gate is not retried.",
61
61
  "Attempts are files under the kiln home, not fields on the job.",
62
+ "Once a pull request is open, tick watches it. A failed check or a review",
63
+ "that requests changes starts a fix on that same branch, up to maxFixRounds",
64
+ "(default 2). Kiln does not merge, approve, or comment. Past that bound the",
65
+ "job is needs-human with reason feedback-exhausted.",
62
66
  "",
63
67
  "serve listens on 127.0.0.1. The page is a board of workflow stages:",
64
68
  "queued, spec, build, review, approve, land, PR review, merged, and needs human.",
package/dist/config.js CHANGED
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "no
2
2
  import { homedir } from "node:os";
3
3
  import { dirname, isAbsolute, join, resolve } from "node:path";
4
4
  import { KilnConfigError } from "./errors.js";
5
- import { DEFAULT_MAX_ATTEMPTS, DEFAULT_STALL_TIMEOUT_MS, KilnConfigFileSchema, } from "./schemas.js";
5
+ import { DEFAULT_MAX_ATTEMPTS, DEFAULT_MAX_FIX_ROUNDS, DEFAULT_STALL_TIMEOUT_MS, KilnConfigFileSchema, } from "./schemas.js";
6
6
  const CONFIG_NAME = "kiln.config.json";
7
7
  const REPO_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
8
8
  export function loadKilnConfig(configPath, options = {}) {
@@ -47,6 +47,7 @@ export function loadKilnConfig(configPath, options = {}) {
47
47
  maxConcurrent: parsed.data.maxConcurrent ?? 2,
48
48
  stallTimeoutMs: parsed.data.stallTimeoutMs ?? DEFAULT_STALL_TIMEOUT_MS,
49
49
  maxAttempts: parsed.data.maxAttempts ?? DEFAULT_MAX_ATTEMPTS,
50
+ maxFixRounds: parsed.data.maxFixRounds ?? DEFAULT_MAX_FIX_ROUNDS,
50
51
  adapter: parsed.data.adapter ?? "claude-code",
51
52
  ...(jira ? { jira } : {}),
52
53
  };