@lchase/kiln 0.6.1 → 0.7.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Lawrence Chase
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -92,7 +92,9 @@ A linked worktree's `.git` file points at the checkout's common git dir, which i
92
92
 
93
93
  Podman works when `docker` is aliased to `podman`. Yak 0.8.0 falls back from the `bridge` egress network to `podman` when `bridge` is absent. `YAK_SANDBOX_EGRESS_NETWORK` overrides that name.
94
94
 
95
- On macOS, Claude subscription auth lives in the keychain. Yak forwards only `ANTHROPIC_API_KEY` or `CLAUDE_CODE_OAUTH_TOKEN` into the container, and never a `~/.claude` directory. Kiln checks that one of those variables is set. It does not print or write the value.
95
+ On macOS, Claude subscription auth lives in the keychain. Yak forwards only `ANTHROPIC_API_KEY` or `CLAUDE_CODE_OAUTH_TOKEN` into the container, and never a `~/.claude` directory. Kiln checks that one of those variables is set before a sandboxed run. It does not print or write the value.
96
+
97
+ Outside the sandbox, an unset `CLAUDE_CODE_OAUTH_TOKEN` is allowed: Claude Code can use the keychain. A variable that is set but empty, or a `CLAUDE_CODE_OAUTH_TOKEN` that does not start with `sk-ant-oat`, makes `kiln tick` and `kiln serve` refuse to launch. The message names the variable and does not include the value. A later 401, `Invalid bearer token`, or `Failed to authenticate` in the yak failure or the kiln log is `auth-failed`. That attempt is not retried. The board says `Claude login rejected (401) — check CLAUDE_CODE_OAUTH_TOKEN / claude login, then Retry`. `kiln retry` is the way back after the token is fixed.
96
98
 
97
99
  ```bash
98
100
  claude setup-token
@@ -117,6 +119,8 @@ npm run kiln -- repo list
117
119
 
118
120
  `npm run kiln` builds first. `kiln repo add <name> <path>` writes `~/.kiln/kiln.config.json`. When `KILN_HOME` is set it writes `$KILN_HOME/kiln.config.json`. `--config <file>` writes that file instead, and jobs live in the file's directory. A relative path is stored absolute, resolved from the working directory at add time. `~` is expanded from the home directory. `test` is optional on the entry and required before tick can launch a job in that repo.
119
121
 
122
+ `repo add` prints the deliver mode in effect and the command that changes it. The default is `local`, and that default is not written into the file until you pass `--deliver`. `kiln repo add <existing-name> <path> --deliver pr` updates that repo in place: a flag you omit keeps the stored test command, deliver mode, base, `prTitle`, and `branch`. `kiln repo list` prints `deliver local` or `deliver pr` for every repo.
123
+
120
124
  ```json
121
125
  {
122
126
  "repos": {
@@ -138,9 +142,10 @@ A job is one workflow run. The workflow name is a file `workflows/<name>.yaml`.
138
142
  npm run kiln -- add feature --repo web --spec spec.md --title "Show a Sunday summary"
139
143
  npm run kiln -- add feature --repo api --spec api-spec.md --after <job-id>
140
144
  npm run kiln -- add feature --repo web --jira PROJ-123
145
+ npm run kiln -- add feature --repo web --spec spec.md --issue ABC-123
141
146
  ```
142
147
 
143
- `--title` defaults to the first `#` heading in the spec, then the file name. `--after` may be repeated. Those jobs must already exist. Kiln copies the spec to `~/.kiln/specs/<id>.md` and writes `~/.kiln/jobs/<id>.json`:
148
+ `--title` defaults to the first `#` heading in the spec, then the file name. `--issue KEY` stores an issue id matching `KEY-123` (uppercased). `--after` may be repeated. Those jobs must already exist. Kiln copies the spec to `~/.kiln/specs/<id>.md` and writes `~/.kiln/jobs/<id>.json`:
144
149
 
145
150
  ```json
146
151
  {
@@ -149,17 +154,18 @@ npm run kiln -- add feature --repo web --jira PROJ-123
149
154
  "workflow": "feature",
150
155
  "spec": "specs/j20261025020600ab12.md",
151
156
  "title": "Show a Sunday summary",
157
+ "issue": "ABC-123",
152
158
  "dependsOn": ["j20261025020500cd34"],
153
159
  "source": { "kind": "file", "ref": "/home/dev/specs/sunday.md" },
154
160
  "createdAt": "2026-10-25T02:06:00.000Z"
155
161
  }
156
162
  ```
157
163
 
158
- `dependsOn` is omitted when the job has no `--after`. `stack: true` is set by `--stack` and is omitted otherwise. `source` records which intake created the job. Jobs written before intake omit `source` and still run. State is not stored. The pull request url is not stored on the job; Kiln writes it under `~/.kiln/deliveries/`.
164
+ `dependsOn` is omitted when the job has no `--after`. `issue` is omitted when intake had no key. `stack: true` is set by `--stack` and is omitted otherwise. `source` records which intake created the job. Jobs written before intake omit `source` and still run. State is not stored. The pull request url is not stored on the job; Kiln writes it under `~/.kiln/deliveries/`.
159
165
 
160
166
  ## Intake
161
167
 
162
- Every intake writes the same job: `id`, `repo`, `workflow`, `spec`, `title`, optional `dependsOn`, and `source`. `source` is `{ "kind": "file" | "prompt" | "tickets" | "jira", "ref": "..." }`. Status prints `source <kind> <ref>` when the job has one. Tick, reconcile, and status do not special-case an intake. The adapters are `src/intake/file.ts`, `src/intake/prompt.ts`, `src/intake/tickets.ts`, and `src/intake/jira.ts`.
168
+ Every intake writes the same job: `id`, `repo`, `workflow`, `spec`, `title`, optional `issue`, optional `dependsOn`, and `source`. `source` is `{ "kind": "file" | "prompt" | "tickets" | "jira", "ref": "..." }`. Status prints `source <kind> <ref>` when the job has one. Tick, reconcile, and status do not special-case an intake. The adapters are `src/intake/file.ts`, `src/intake/prompt.ts`, `src/intake/tickets.ts`, and `src/intake/jira.ts`.
163
169
 
164
170
  ### File
165
171
 
@@ -185,7 +191,7 @@ npm run kiln -- import ./tickets --repo web
185
191
  npm run kiln -- import ./tickets --repo web --workflow feature --context spec.md --dry-run
186
192
  ```
187
193
 
188
- `import` reads a directory of `NN-slug.md` files, the shape a Matt Pocock-style `/to-tickets` pass writes. Each file starts with `# NN: Title`, a `**What to build:**` paragraph, and a `**Blocked by:**` line. That line is followed by `None…` or by bullets such as `- 01: Probe the unconfirmed platform facts`. Kiln matches the `NN` number to a filename in the same directory (`01` and `1` are the same number). YAML front matter is also accepted, with `title`, `type`, and `blocked-by` (`blocked-by:` as a list, a flow list, or a single number).
194
+ `import` reads a directory of `NN-slug.md` files, the shape a Matt Pocock-style `/to-tickets` pass writes. Each file starts with `# NN: Title`, a `**What to build:**` paragraph, and a `**Blocked by:**` line. That line is followed by `None…` or by bullets such as `- 01: Probe the unconfirmed platform facts`. Kiln matches the `NN` number to a filename in the same directory (`01` and `1` are the same number). YAML front matter is also accepted, with `title`, `type`, `issue`, and `blocked-by` (`blocked-by:` as a list, a flow list, or a single number). `issue` must be a `KEY-123` id. A heading or title that already starts with `ABC-123:`, `ABC-123 `, or `[ABC-123]` sets the same field. A numbered heading such as `# 01: Title` does not. The stored title keeps that prefix; the pull request template strips it.
189
195
 
190
196
  One file becomes one job. Blocked-by becomes `dependsOn` on the imported job ids. An unknown number is an error that names the ticket file, and no new job is written.
191
197
 
@@ -202,7 +208,7 @@ npm run kiln -- add feature --repo web --jira PROJ-123
202
208
  npm run kiln -- add feature --repo web --jira PROJ-123 --after <job-id> --dry-run
203
209
  ```
204
210
 
205
- `--jira` reads one Jira Cloud issue over REST API v3 and writes the same job as the other intakes. `source.kind` is `jira`. `source.ref` is the issue key (`PROJ-123`). The title is `PROJ-123: <summary>`. The spec is that heading, a `Source:` line with the issue's browse URL, then the description converted from Atlassian document format to markdown.
211
+ `--jira` reads one Jira Cloud issue over REST API v3 and writes the same job as the other intakes. `source.kind` is `jira`. `source.ref` is the issue key (`PROJ-123`). The job's `issue` is that key. The title is the issue summary, or `--title` when you pass one. The spec heading stays `# PROJ-123: <summary>`, then a `Source:` line with the issue's browse URL, then the description converted from Atlassian document format to markdown. `--issue` on the same command must be that key.
206
212
 
207
213
  Set `JIRA_BASE_URL` (for example `https://acme.atlassian.net`), `JIRA_EMAIL`, and `JIRA_API_TOKEN`. Each value falls back to the optional `jira` block in the kiln config (`~/.kiln/kiln.config.json`, or `$KILN_HOME/kiln.config.json` when `KILN_HOME` is set, or the file passed with `--config`). Environment variables override the block:
208
214
 
@@ -228,17 +234,21 @@ kiln serve --port 4100 --tick 30
228
234
  kiln serve --no-tick
229
235
  ```
230
236
 
231
- `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.
237
+ `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, retry, add, and import functions. Nothing new is stored.
238
+
239
+ Before each request and each tick, serve stats the kiln config and reloads it when the mtime or size changed. `kiln repo add`, a sandbox mode change, and `maxAttempts` apply on the next request or tick. Leave serve running. When the file does not parse, serve keeps the last good config and shows one monospace line on the board with the config path and the validation message. That line goes away when the file is valid again. Secret redaction uses the reloaded config.
232
240
 
233
- `/` 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.
241
+ `/` 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, Done, Merged, and Needs human. Done (`local`) is a landed job with no pull request: `deliver=local`, or an investigation. The card says `landed locally · yak/<run-id>`. Merged (`pr`) is a done job whose pull request exists. The card says `merged · PR #N`. An empty board shows Done and does not label that column Merged. Both columns appear when the kiln has one of each. A card's repo line includes the deliver mode, `web · local` or `web · pr`. 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`. Add job sits in that toolbar and opens a drawer, not a second page: repo chips, a workflow from `workflows/*.yaml`, then Prompt, Spec file, Tickets folder, or Jira. An optional issue id (`ABC-123`) is the same field as `kiln add --issue`. A Jira source fills it from the issue key. The title starts as the spec's first heading or the prompt's first line, and stays editable. After lists jobs that are not done. Submit closes the drawer and highlights the new card in Queued. A tickets folder can be previewed first; the drawer shows the jobs and the dependency graph and writes nothing until Add. The file list starts at the selected repo when that path is under the home directory, and remembers the last directory 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.
234
242
 
235
243
  `GET /jobs/<id>/findings` renders the approved findings markdown as text. HTML in that file is escaped. The page is read-only.
236
244
 
237
- `GET /api/jobs` and `GET /api/jobs/<id>` return that view as JSON, including the attention sections. `POST /api/jobs/<id>/approve`, `POST /api/jobs/<id>/reject`, and `POST /api/jobs/<id>/retry` call the same functions as the CLI. A browser POST from another origin is rejected. The page and the stylesheet ship inside the package (`assets/serve.css`, read from the package root).
245
+ `GET /api/jobs` and `GET /api/jobs/<id>` return that view as JSON, including the attention sections. `GET /api/status` is that same list plus `repos` (name and path) and `configError` (`null`, or the validation message). `GET /api/jobs` carries those two fields too, and the page polls it every 3 seconds, so a repo added from the CLI shows up on the Add job chips without a separate reload. `POST /api/jobs` adds work through the intake adapters. The body is `{ repo, workflow, title?, issue?, after?, stack?, dryRun?, source }` where `source` is `{ kind: "prompt", text }`, `{ kind: "spec", path }`, `{ kind: "tickets", dir, context? }`, or `{ kind: "jira", key }`. `issue` is an optional `KEY-123` id, the same field `kiln add --issue` stores. With `deliver=pr` the draft title is `[ABC-123] Title` and the remote branch is `ABC-123-slug`. A Jira source stores the issue key when `issue` is omitted, and rejects a different key. A tickets import does not take `issue`; each file has its own. `dryRun: true` previews a tickets import and writes nothing. The response is `{ ids: string[] }`. Spec and ticket paths must be absolute and under the home directory.
246
+
247
+ `POST /api/jobs/<id>/approve`, `POST /api/jobs/<id>/reject`, and `POST /api/jobs/<id>/retry` call the same functions as the CLI. Every POST requires the CSRF token embedded in the page (`X-Kiln-CSRF`, or a `csrf` field on the approve forms). A request whose Origin or Host is not this localhost port is rejected. That check also covers `GET /api/fs`. `GET /api/fs?path=` lists directories and `.md` files under the home directory. A chosen `.md` file returns its first 40 lines. Paths outside the home directory, hidden directories, and other files are refused. The page and the stylesheet ship inside the package (`assets/serve.css`, `assets/serve.js`, read from the package root).
238
248
 
239
249
  `kiln retry <jobId>` relaunches a failed or dead job with the same launch tick uses, and clears `<kiln-dir>/attempts/<job-id>.json`. A landed job that only failed to open its pull request is delivered again instead. The board's Retry button calls that same function.
240
250
 
241
- By default, serve runs `kiln tick` once when it starts and then every 30 seconds. `--tick <seconds>` changes the interval. `--no-tick` leaves the factory where it is. The tick uses the same lock as `kiln tick`, so a second tick exits with "another kiln tick is running" instead of launching twice. There is no intake form in this UI yet.
251
+ By default, serve runs `kiln tick` once when it starts and then every 30 seconds. `--tick <seconds>` changes the interval. `--no-tick` leaves the factory where it is. The tick uses the same lock as `kiln tick`, so a second tick exits with "another kiln tick is running" instead of launching twice. The tick re-reads the config first, so a repo registered while serve is open is launchable on the next tick.
242
252
 
243
253
  The Jira API token is not rendered. Do not commit checkout paths or job files.
244
254
 
@@ -257,13 +267,13 @@ npm run kiln -- reject <job-id>
257
267
  while true; do npm run kiln -- tick; sleep 30; done
258
268
  ```
259
269
 
260
- 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.
270
+ 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. An authentication failure does not either: reason `auth-failed` stays in Needs human until `kiln retry`.
261
271
 
262
272
  `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.
263
273
 
264
274
  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.
265
275
 
266
- `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.
276
+ `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 the branch. `deliver=local` prints `branch yak/<run-id>`. `deliver=pr` prints the remote branch name recorded on the delivery, and the pull request url once one exists. `auth-failed` prints `Claude login rejected (401) — check CLAUDE_CODE_OAUTH_TOKEN / claude login, then Retry`. 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.
267
277
 
268
278
  State is derived from the yak journal, the gate files, and (for `deliver=pr`) `gh pr view`:
269
279
 
@@ -275,7 +285,7 @@ State is derived from the yak journal, the gate files, and (for `deliver=pr`) `g
275
285
  | `needs-human` | The run is suspended on an open gate, or pull request feedback is exhausted (`feedback-exhausted`) or ambiguous (`feedback-ambiguous`) |
276
286
  | `approved` | The gate says approve and the run has not finished. This still holds a slot |
277
287
  | `rejected` | The run finished ok and the gate says reject |
278
- | `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 |
288
+ | `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, a Claude login was rejected (`auth-failed`, not retried), or a transient failure is waiting to retry |
279
289
  | `dead` | The attempt budget is used up. Before the next tick, a journal whose process is already gone is also `dead`. Status prints the cancel command when a process is gone, and `kiln retry <id>` once attempts are exhausted |
280
290
  | `in-review` | `deliver=pr` and the run has landed. The pull request is open, or the next tick still has to open it |
281
291
  | `done` | `deliver=local`: the run finished ok and the gate says approve. `deliver=pr`: that pull request has merged. An investigation is `done` when the gate says approve, including when the repo is `deliver=pr`. It does not enter `in-review` |
@@ -286,7 +296,7 @@ State is derived from the yak journal, the gate files, and (for `deliver=pr`) `g
286
296
 
287
297
  The job runs the factory loop from Lawrence Chase's factory plugin v0.7.0 (MIT), copied from commit `4c84a2ce7fbde5ac298c0de9f0b90141eac4ed98`: spec, then up to two rounds of build, verify, and the security, ux, ui, and code reviewers, then the approver, a human gate, and a commit. The prompts in `prompts/factory/` are those agent files, copied verbatim. `prompts/kiln/` maps the factory's job-folder files onto the artifacts yak passes in. `npm run kiln -- tick` writes a per-run copy of the workflow under the kiln directory, with absolute prompt paths, before it starts yak.
288
298
 
289
- Verify runs the repo's `test` command with `failOn: never`. Stdout and stderr stored from verify are cut to the last 200 lines or 20KB. One round is build, then verify, then the four reviewers. `concurrency: 4` starts security, ux, ui, and code together after verify. Build stays first. The approver runs after the loop. Each reviewer needs `spec`, `build`, and `verify`, and inherits `input` so the preamble can still read `baseRef`. Yak warns that those four Bash steps share one working tree and may race. That warning is expected: the reviewer preambles say to use the verify output and not re-run tests, builds, or anything else that writes. Git diff and file reads stay allowed. A `CHANGES` verdict, a `BLOCKED` build, or a non-zero verify starts another build, at most twice. The loop then continues to the approver. After an approved gate, the run commits on `yak/<run-id>` with message `kiln: <title>`. A reject skips that commit. `adapter: "mock"` reads fixtures at `test/fixtures/kiln-feature/<step>.json` and does not call a model.
299
+ Verify runs the repo's `test` command with `failOn: never`. Stdout and stderr stored from verify are cut to the last 200 lines or 20KB. One round is build, then verify, then the four reviewers. `concurrency: 4` starts security, ux, ui, and code together after verify. Build stays first. The approver runs after the loop. Each reviewer needs `spec`, `build`, and `verify`, and inherits `input` so the preamble can still read `baseRef`. The reviewer prompts run `git diff <main branch>...HEAD`, so Bash stays. The kiln preambles tell them to use the verify output and not re-run tests, builds, or anything else that writes. Yak 0.8.0 still warns: it treats Bash as write-capable (`WRITE_CAPABLE_TOOLS` in `src/ir/validate.ts`) and has no way to mark a shell step read-only. The only fixes yak offers are `concurrency: 1` or dropping Bash. Neither is right here. Parallel reviews stay, and silencing that warning needs a yak change. A `CHANGES` verdict, a `BLOCKED` build, or a non-zero verify starts another build, at most twice. The loop then continues to the approver. After an approved gate, the run commits on `yak/<run-id>` with message `kiln: <title>`. A reject skips that commit. `adapter: "mock"` reads fixtures at `test/fixtures/kiln-feature/<step>.json` and does not call a model.
290
300
 
291
301
  The first launch adds `.runs/` and `.yak/` to that checkout's `.git/info/exclude`.
292
302
 
@@ -304,15 +314,32 @@ A later job whose `dependsOn` includes that investigation receives the findings
304
314
 
305
315
  ## Delivery and dependencies
306
316
 
307
- `deliver` is set on the repo. The default is `local`: the job is `done` when the run lands on `yak/<run-id>`, and Kiln does not push. Existing repos and tests stay on that path.
317
+ `deliver` is set on the repo. The default is `local`: the job is `done` when the run lands on `yak/<run-id>`, and Kiln does not push. Existing repos and tests stay on that path. `kiln repo add` prints that mode. `kiln repo list` shows it. The board card does too.
308
318
 
309
319
  ```bash
310
320
  npm run kiln -- repo add web /absolute/path/to/web --test "npm test" --deliver pr --base main
311
321
  ```
312
322
 
313
- `deliver` is `local` or `pr`. `base` is the pull request base branch. When `base` is omitted, Kiln uses origin's default branch, from `refs/remotes/origin/HEAD` after `git fetch`.
323
+ `deliver` is `local` or `pr`. `base` is the pull request base branch. When `base` is omitted, Kiln uses origin's default branch, from `refs/remotes/origin/HEAD` after `git fetch`. Running `repo add` again for the same name updates the entry in place.
324
+
325
+ With `deliver=pr`, the tick after an approved land pushes the local `yak/<run-id>` ref to a readable remote branch and runs `gh pr create --draft --head <that name>`. Yak keeps the local branch. The delivery record stores the remote name, and later ticks, including a fix round that fast-forwards the open pull request, use that name. A name already recorded for this run is reused, including an older record that stored `yak/<run-id>`.
326
+
327
+ The default remote name is `<issue>-<slug>` when the job has an issue, and `kiln/<slug>-<short id>` otherwise. `slug` is the title in kebab case, at most 50 characters, with a leading issue prefix removed. `short id` is the four hex digits at the end of a kiln job id. A remote branch that already points at a different commit gets `-<short id>` appended, or the full job id when the short id is already on the name. A branch that already points at this commit is ours.
328
+
329
+ The default pull request title is `[ABC-123] Title` when `issue` is set: template `[{issue}] {title}`. Before that template is applied, a leading `ABC-123:`, `ABC-123 `, or `[ABC-123]` is stripped, so a Jira `KEY: summary` title is not written twice. A job with no issue keeps its title, and a repo `prTitle` template is ignored in that case. Set `prTitle` on the repo (`{issue}` and `{title}`) for another convention. Set `branch` (`{issue}`, `{slug}`, `{jobId}`) for another remote name.
330
+
331
+ ```json
332
+ "web": {
333
+ "path": "/absolute/path/to/web",
334
+ "test": "npm test",
335
+ "deliver": "pr",
336
+ "base": "main",
337
+ "prTitle": "[{issue}] {title}",
338
+ "branch": "{issue}-{slug}"
339
+ }
340
+ ```
314
341
 
315
- With `deliver=pr`, the tick after an approved land pushes `yak/<run-id>` and runs `gh pr create --draft`. The title is the job title. The body is the spec's first paragraph, the line `Kiln job: <id>`, and the security, ux, ui, code, and approver verdicts. The branch, pull request number, and url are written to `~/.kiln/deliveries/<job-id>.json`, not the job file. A later tick looks up an existing pull request for that branch and does not push when origin already has the same commit. Kiln does not merge, and it does not push the base branch. An investigation is not delivered this way: approve stores findings and the job is `done`.
342
+ The body is the spec's first paragraph, the line `Kiln job: <id>`, and the security, ux, ui, code, and approver verdicts. The branch, pull request number, and url are written to `~/.kiln/deliveries/<job-id>.json`, not the job file. A later tick looks up an existing pull request for that remote branch and does not push when origin already has the same commit. Deleting the delivery file and ticking again finds that pull request by the remote name. Kiln does not merge, and it does not push the base branch. An investigation is not delivered this way: approve stores findings and the job is `done`.
316
343
 
317
344
  If `gh` is missing or not authenticated, that job becomes `failed` and status prints what to run (`gh auth login`, then `kiln tick`). The rest of the tick still runs. The next tick tries again.
318
345
 
@@ -322,7 +349,7 @@ If `gh` is missing or not authenticated, that job becomes `failed` and status pr
322
349
 
323
350
  The default is after merged. A job stays `blocked` until every upstream is `done`. Status says `waiting for PR #N to merge` while an upstream pull request is open. A `deliver=pr` worktree is cut from `origin/<base>` after `git fetch`, not from checkout `HEAD`.
324
351
 
325
- `--stack` on `kiln add` or `kiln import` waits for one upstream to be approved and landed, then cuts the worktree from that upstream branch (`yak/<run-id>`). The pull request targets that branch. `stack` with any count other than one dependency is an error at add time, and the upstream job has to be in the same repo. Stacking onto an investigation is an error, because that job does not land a branch. Stacked work does not wait for the upstream pull request to merge.
352
+ `--stack` on `kiln add` or `kiln import` waits for one upstream to be approved and landed, then cuts the worktree from that upstream's local `yak/<run-id>` branch. The pull request targets the upstream's remote branch name. `stack` with any count other than one dependency is an error at add time, and the upstream job has to be in the same repo. Stacking onto an investigation is an error, because that job does not land a branch. Stacked work does not wait for the upstream pull request to merge.
326
353
 
327
354
  ```bash
328
355
  npm run kiln -- add feature --repo web --spec spec.md --after <job-id> --stack
package/assets/serve.css CHANGED
@@ -190,6 +190,7 @@ h1 { margin: 0; font-size: 18px; line-height: 24px; font-weight: 500; }
190
190
  .card.retrying .meta { color: var(--faint); }
191
191
  .card.queued:hover,
192
192
  .card.retrying:hover { border-color: var(--line-strong); }
193
+ .card.fresh { border-color: var(--live); box-shadow: 0 0 0 1px var(--live); }
193
194
  .card-top { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; }
194
195
  .num {
195
196
  font-family: var(--mono);
@@ -431,6 +432,98 @@ body.open .drawer { transform: none; }
431
432
  .verdict.warn { color: var(--changes); background: color-mix(in srgb, var(--changes) 18%, transparent); }
432
433
  .verdict.stop { color: var(--human); background: color-mix(in srgb, var(--human) 18%, transparent); }
433
434
 
435
+ .config-error { margin: 0; padding: 8px 24px 0; color: var(--human); }
436
+ .sheet.intake { height: 100%; display: flex; flex-direction: column; }
437
+ .intake-form {
438
+ flex: 1;
439
+ min-height: 0;
440
+ overflow: auto;
441
+ display: flex;
442
+ flex-direction: column;
443
+ gap: 14px;
444
+ padding: 16px 24px 48px;
445
+ max-width: 560px;
446
+ }
447
+ .intake-form .field { display: flex; flex-direction: column; gap: 6px; margin: 0; border: 0; padding: 0; min-width: 0; }
448
+ .intake-form .k, .intake-form legend.k {
449
+ font-family: var(--mono);
450
+ font-size: 12px;
451
+ line-height: 16px;
452
+ color: var(--muted);
453
+ padding: 0;
454
+ }
455
+ .intake-form select,
456
+ .intake-form input[type="text"],
457
+ .intake-form input[type="number"],
458
+ .intake-form textarea {
459
+ width: 100%;
460
+ font: 14px/20px Roboto, sans-serif;
461
+ color: var(--ink);
462
+ background: var(--bg);
463
+ border: 1px solid var(--line-strong);
464
+ border-radius: 4px;
465
+ padding: 8px 10px;
466
+ }
467
+ .intake-form textarea { resize: vertical; min-height: 88px; }
468
+ .picks, .seg { display: flex; flex-wrap: wrap; gap: 6px; }
469
+ .pick, .seg button, .fs-up, .fs-use {
470
+ height: 28px;
471
+ padding: 0 12px;
472
+ border: 1px solid var(--line-strong);
473
+ border-radius: 4px;
474
+ background: none;
475
+ color: var(--muted);
476
+ font: 12px/16px var(--mono);
477
+ cursor: pointer;
478
+ }
479
+ .pick[aria-pressed="true"], .seg button[aria-pressed="true"] {
480
+ border-color: transparent;
481
+ background: color-mix(in srgb, var(--merged) 22%, transparent);
482
+ color: var(--ink);
483
+ }
484
+ .pick:hover, .seg button:hover, .fs-up:hover, .fs-use:hover, .fs-item:hover { background: var(--card-hover); color: var(--ink); }
485
+ .fs { display: flex; flex-direction: column; gap: 8px; }
486
+ .fs-list { display: flex; flex-direction: column; max-height: 220px; overflow: auto; border: 1px solid var(--line); border-radius: 4px; }
487
+ .fs-item {
488
+ text-align: left;
489
+ background: none;
490
+ border: 0;
491
+ border-bottom: 1px solid var(--line);
492
+ color: var(--ink);
493
+ font: 12px/16px var(--mono);
494
+ padding: 6px 10px;
495
+ cursor: pointer;
496
+ }
497
+ .fs-item:last-child { border-bottom: 0; }
498
+ #after-list { max-height: 140px; overflow: auto; }
499
+ .after { display: flex; gap: 8px; align-items: baseline; margin: 4px 0; font-size: 13px; cursor: pointer; }
500
+ .after .num { font-family: var(--mono); font-size: 12px; color: var(--muted); }
501
+ .form-error { margin: 0; color: var(--human); font-size: 12px; line-height: 16px; }
502
+ .intake-form .preview { margin: 0; max-height: 200px; overflow: auto; }
503
+ .intake-form .d-actions button[type="submit"] {
504
+ color: var(--ink);
505
+ background: var(--primary);
506
+ border-color: transparent;
507
+ }
508
+ #settings-form { max-width: 640px; }
509
+ #settings-path { margin: 0; }
510
+ .saved { margin: 0; color: var(--pass); font-family: var(--mono); font-size: 12px; line-height: 16px; }
511
+ .hint { margin: 0; color: var(--faint); font-size: 12px; line-height: 16px; }
512
+ .field-error { margin: 0; color: var(--human); font-size: 12px; line-height: 16px; }
513
+ .path-row { display: flex; gap: 8px; align-items: center; }
514
+ .path-row input { flex: 1; min-width: 0; }
515
+ .repo-row {
516
+ display: flex;
517
+ justify-content: space-between;
518
+ gap: 12px;
519
+ align-items: flex-start;
520
+ padding: 10px 0;
521
+ border-bottom: 1px solid var(--line);
522
+ }
523
+ .repo-row .num { font-family: var(--mono); font-size: 13px; }
524
+ .repo-row .d-actions { margin-top: 0; flex: none; }
525
+ #repo-editor { padding-top: 8px; }
526
+ #settings-error { white-space: pre-wrap; }
434
527
  .back { display: inline-block; margin: 24px 24px 0; }
435
528
  .page { padding: 0 24px 48px; }
436
529
  .text { font-family: var(--mono); font-size: 13px; white-space: pre-wrap; overflow-wrap: anywhere; }