@lchase/kiln 0.4.0 → 0.6.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 +61 -5
- package/dist/attention.js +28 -2
- package/dist/cli.js +30 -0
- package/dist/config.js +14 -1
- package/dist/driver.js +484 -54
- package/dist/feedback.js +610 -0
- package/dist/host.js +26 -1
- package/dist/reconcile.js +31 -3
- package/dist/reliability.js +16 -0
- package/dist/report.js +76 -9
- package/dist/sandbox.js +363 -0
- package/dist/schemas.js +26 -0
- package/dist/serve-page.js +14 -1
- package/dist/serve.js +7 -1
- package/dist/workflow.js +2 -0
- package/package.json +1 -1
- package/prompts/kiln/fix.md +17 -0
- package/workflows/feature.yaml +5 -0
- package/workflows/fix.yaml +48 -0
- package/workflows/investigation.yaml +3 -0
package/README.md
CHANGED
|
@@ -42,6 +42,60 @@ Workflows (`workflows/*.yaml`) and prompts (`prompts/`) are read from the packag
|
|
|
42
42
|
|
|
43
43
|
Each checkout is a git repo with a commit at `HEAD`. `tick` adds a worktree and leaves uncommitted files in the checkout as they are. The job edits the worktree, on branch `yak/<run-id>`. With the default `deliver: "local"`, Kiln does not push. `deliver: "pr"` pushes that branch and opens a draft pull request. See [Delivery and dependencies](#delivery-and-dependencies).
|
|
44
44
|
|
|
45
|
+
## Sandbox
|
|
46
|
+
|
|
47
|
+
Sandbox mode is off. Set it in the kiln config when agent steps should run inside yak's Docker sandbox instead of on the machine. A workflow block overrides the global one field by field. `mode` omitted means off at that layer. `allowDomains`, when set, replaces the list.
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"sandbox": { "mode": "docker" },
|
|
52
|
+
"workflows": {
|
|
53
|
+
"investigation": { "sandbox": { "mode": "off" } }
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`image` overrides `yak-agent-sandbox:latest`. Leave it unset to use that tag.
|
|
59
|
+
|
|
60
|
+
With mode `docker`, tick injects `sandbox: docker` and `allowedDomains` under every agent step in the rendered workflow. The default allowlist is exact hostnames, matched by the proxy as `dstdomain` (a subdomain is not implied):
|
|
61
|
+
|
|
62
|
+
- `api.anthropic.com`
|
|
63
|
+
- `registry.npmjs.org`
|
|
64
|
+
- `github.com`
|
|
65
|
+
- `api.github.com`
|
|
66
|
+
- `codeload.github.com`
|
|
67
|
+
- `objects.githubusercontent.com`
|
|
68
|
+
- `raw.githubusercontent.com`
|
|
69
|
+
|
|
70
|
+
That is the Anthropic API, the npm registry, and the GitHub hosts a fetch or install dials. It is not the open internet.
|
|
71
|
+
|
|
72
|
+
| Step | Where it runs |
|
|
73
|
+
| --- | --- |
|
|
74
|
+
| feature: spec, build, security, ux, ui, code, approver | Docker sandbox |
|
|
75
|
+
| investigation: explore | Docker sandbox |
|
|
76
|
+
| fix: build | Docker sandbox. A pull-request fix uses the same allowlist. |
|
|
77
|
+
| feature: verify | Host. It runs `KILN_TEST` through Kiln's truncate wrapper. Yak's command sandbox does not forward that environment and uses `--network none`. |
|
|
78
|
+
| approve gates | Host. A person answers them. |
|
|
79
|
+
| feature: land, fix: land | Host. `git commit` uses the host git identity. Publishing the fix branch is the kiln process. |
|
|
80
|
+
| Pull request open, `git push`, `gh pr view`, feedback reads | Host kiln process. They are not workflow steps. |
|
|
81
|
+
|
|
82
|
+
`kiln sandbox` prints the resolved mode, the allowlist, and anything still missing. `kiln sandbox build` builds the images yak ships but does not publish, from the tag that matches the installed `@lchase/yak`:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
docker build -t yak-agent-sandbox:latest https://github.com/lchase/yak.git#yak-v0.7.1:docker/agent-sandbox
|
|
86
|
+
docker build -t yak-agent-sandbox-proxy:latest https://github.com/lchase/yak.git#yak-v0.7.1:docker/agent-proxy
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The agent image is `node:22-slim` plus the Claude CLI at `/app/claude-cli`. It has no git. `kiln sandbox build` adds `git` and `ca-certificates` and tags that result `yak-agent-sandbox:latest` as well. The proxy image is `yak-agent-sandbox-proxy:latest` (squid on port 3128). Yak mounts the worktree at `/workspace` and puts the container on an `--internal` network whose only egress is that proxy.
|
|
90
|
+
|
|
91
|
+
A linked worktree's `.git` file points at the checkout's common git dir, which is outside that mount. `git` inside the container cannot see the repository until yak mounts that dir too. Land stays on the host, where the worktree link works.
|
|
92
|
+
|
|
93
|
+
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. Before the first sandboxed launch, run `claude setup-token` and export `CLAUDE_CODE_OAUTH_TOKEN` in the environment that `kiln tick` or `kiln serve` inherits. Kiln checks that the variable is set. It does not print or write the value. An API key in `ANTHROPIC_API_KEY` works the same way.
|
|
94
|
+
|
|
95
|
+
`kiln tick` and `kiln serve` name what is missing when sandbox mode is on: the docker binary, the docker daemon, each image, the credential, and a yak build that actually reads `allowedDomains` from the workflow. Installed `@lchase/yak` 0.7.1 drops `sandbox`, `image`, and `allowedDomains` while loading YAML, and opens the proxy with only `api.anthropic.com`. Kiln refuses to launch in that case, because the job would run on the host with the open internet. The job stays failed until `kiln retry`, after that piece is fixed. The yak change that copies those fields and passes `allowedDomains` into `createAgentSandboxNetwork` has to be in the installed package first.
|
|
96
|
+
|
|
97
|
+
A step that fails with yak reason `sandbox-error` goes to Needs human. The card shows the failure detail. Kiln does not schedule an automatic retry for it. The card of a sandboxed job shows `sandbox · docker` in the same monospace metadata as the rest of the board.
|
|
98
|
+
|
|
45
99
|
## Register repos
|
|
46
100
|
|
|
47
101
|
```bash
|
|
@@ -68,7 +122,7 @@ npm run kiln -- repo list
|
|
|
68
122
|
|
|
69
123
|
## Add a job
|
|
70
124
|
|
|
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.
|
|
125
|
+
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
126
|
|
|
73
127
|
```bash
|
|
74
128
|
npm run kiln -- add feature --repo web --spec spec.md --title "Show a Sunday summary"
|
|
@@ -166,7 +220,7 @@ kiln serve --no-tick
|
|
|
166
220
|
|
|
167
221
|
`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
222
|
|
|
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.
|
|
223
|
+
`/` 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
224
|
|
|
171
225
|
`GET /jobs/<id>/findings` renders the approved findings markdown as text. HTML in that file is escaped. The page is read-only.
|
|
172
226
|
|
|
@@ -195,7 +249,9 @@ while true; do npm run kiln -- tick; sleep 30; done
|
|
|
195
249
|
|
|
196
250
|
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
251
|
|
|
198
|
-
`stallTimeoutMs` (default `900000`, 15 minutes)
|
|
252
|
+
`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.
|
|
253
|
+
|
|
254
|
+
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
255
|
|
|
200
256
|
`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
257
|
|
|
@@ -206,7 +262,7 @@ State is derived from the yak journal, the gate files, and (for `deliver=pr`) `g
|
|
|
206
262
|
| `queued` | Dependencies are met, and the job has not started |
|
|
207
263
|
| `blocked` | An after-merged dependency is not `done`, or a stacked dependency has not landed. A missing job id is named |
|
|
208
264
|
| `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 |
|
|
265
|
+
| `needs-human` | The run is suspended on an open gate, or pull request feedback is exhausted (`feedback-exhausted`) or ambiguous (`feedback-ambiguous`) |
|
|
210
266
|
| `approved` | The gate says approve and the run has not finished. This still holds a slot |
|
|
211
267
|
| `rejected` | The run finished ok and the gate says reject |
|
|
212
268
|
| `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 +321,4 @@ npm run kiln -- import ./tickets --repo web --stack
|
|
|
265
321
|
|
|
266
322
|
## Not included
|
|
267
323
|
|
|
268
|
-
No requirement refinement. Bug
|
|
324
|
+
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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { INVESTIGATION_WORKFLOW } from "./findings.js";
|
|
3
|
+
import { SANDBOX_PREFLIGHT_PREFIX } from "./sandbox.js";
|
|
3
4
|
/**
|
|
4
5
|
* Attention order for `kiln serve`. Built from the same derived jobs as
|
|
5
6
|
* `kiln status`. The console renders this and does not classify jobs again.
|
|
@@ -97,13 +98,17 @@ function toItem(job) {
|
|
|
97
98
|
export function attentionReason(job, tail = errorTail(job)) {
|
|
98
99
|
switch (job.state) {
|
|
99
100
|
case "needs-human": {
|
|
101
|
+
if (job.feedbackReason)
|
|
102
|
+
return job.feedbackReason;
|
|
100
103
|
const question = gateQuestion(job);
|
|
101
104
|
return question ? `Gate: ${oneLine(question)}` : "Waiting on a person";
|
|
102
105
|
}
|
|
103
106
|
case "dead":
|
|
104
|
-
return "Run died before it finished";
|
|
107
|
+
return job.sandboxError ? `Sandbox: ${oneLine(job.sandboxError)}` : "Run died before it finished";
|
|
105
108
|
case "failed": {
|
|
106
|
-
|
|
109
|
+
if (job.sandboxError)
|
|
110
|
+
return `Sandbox: ${oneLine(job.sandboxError)}`;
|
|
111
|
+
const why = failureLine(job.launchError) || firstLine(job.deliveryError) || firstLine(job.failureDetail) || firstLine(tail) || "Run failed";
|
|
107
112
|
return `Failed: ${oneLine(why)}`;
|
|
108
113
|
}
|
|
109
114
|
case "in-review":
|
|
@@ -225,6 +230,8 @@ function attentionRevision(needsYou, inFlight, doneRecently, graph) {
|
|
|
225
230
|
item.job.attemptCount,
|
|
226
231
|
item.job.lastFailureReason ?? "",
|
|
227
232
|
item.job.nextRetryAt ?? "",
|
|
233
|
+
feedbackStamp(item.job),
|
|
234
|
+
item.job.sandboxed ? "sandbox" : "",
|
|
228
235
|
]),
|
|
229
236
|
inFlight: inFlight.map((item) => [
|
|
230
237
|
item.job.jobId,
|
|
@@ -234,12 +241,20 @@ function attentionRevision(needsYou, inFlight, doneRecently, graph) {
|
|
|
234
241
|
item.job.attemptCount,
|
|
235
242
|
item.job.lastFailureReason ?? "",
|
|
236
243
|
item.job.nextRetryAt ?? "",
|
|
244
|
+
feedbackStamp(item.job),
|
|
245
|
+
item.job.sandboxed ? "sandbox" : "",
|
|
237
246
|
]),
|
|
238
247
|
doneRecently: doneRecently.map((item) => [item.job.jobId, item.job.state]),
|
|
239
248
|
edges: graph.edges.map((edge) => [edge.from, edge.to, edge.stack]),
|
|
240
249
|
});
|
|
241
250
|
return createHash("sha256").update(body).digest("hex").slice(0, 16);
|
|
242
251
|
}
|
|
252
|
+
function feedbackStamp(job) {
|
|
253
|
+
const rounds = (job.feedbackRounds ?? [])
|
|
254
|
+
.map((round) => `${round.round}:${round.trigger}:${round.outcome ?? ""}:${round.headSha}`)
|
|
255
|
+
.join("|");
|
|
256
|
+
return `${job.feedbackReason ?? ""}:${rounds}`;
|
|
257
|
+
}
|
|
243
258
|
function oneLine(text, max = 180) {
|
|
244
259
|
const flat = text.replace(/\s+/g, " ").trim();
|
|
245
260
|
if (flat.length <= max)
|
|
@@ -252,3 +267,14 @@ function firstLine(text) {
|
|
|
252
267
|
const line = text.split("\n").find((candidate) => candidate.trim().length > 0);
|
|
253
268
|
return line?.trim();
|
|
254
269
|
}
|
|
270
|
+
/** The preflight header names the policy. The card should name the missing piece. */
|
|
271
|
+
function failureLine(text) {
|
|
272
|
+
const line = firstLine(text);
|
|
273
|
+
if (!line?.startsWith(SANDBOX_PREFLIGHT_PREFIX))
|
|
274
|
+
return line;
|
|
275
|
+
const bullet = text
|
|
276
|
+
?.split("\n")
|
|
277
|
+
.map((candidate) => candidate.trim())
|
|
278
|
+
.find((candidate) => candidate.startsWith("- "));
|
|
279
|
+
return bullet ? bullet.slice(2) : line;
|
|
280
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { addRepo, renderRepoList, resolveKilnConfig } from "./config.js";
|
|
3
|
+
import { renderSandboxStatus, runSandboxBuild } from "./sandbox.js";
|
|
3
4
|
import { answerJob, retryJob, runStatus, runTick } from "./driver.js";
|
|
4
5
|
import { startServe } from "./serve.js";
|
|
5
6
|
import { addFromFile } from "./intake/file.js";
|
|
@@ -18,6 +19,7 @@ function usage() {
|
|
|
18
19
|
" kiln approve <jobId> [--config <file>]",
|
|
19
20
|
" kiln reject <jobId> [--config <file>]",
|
|
20
21
|
" kiln retry <jobId> [--config <file>]",
|
|
22
|
+
" kiln sandbox [build] [--config <file>]",
|
|
21
23
|
" kiln serve [--port 4100] [--tick <seconds>|--no-tick] [--config <file>]",
|
|
22
24
|
"",
|
|
23
25
|
"repo add registers a checkout. A relative path is stored absolute, resolved",
|
|
@@ -59,12 +61,21 @@ function usage() {
|
|
|
59
61
|
"after 10s, 20s, 40s, and so on, capped at 5 minutes, up to maxAttempts",
|
|
60
62
|
"(default 3). The job is then dead. A rejected gate is not retried.",
|
|
61
63
|
"Attempts are files under the kiln home, not fields on the job.",
|
|
64
|
+
"Once a pull request is open, tick watches it. A failed check or a review",
|
|
65
|
+
"that requests changes starts a fix on that same branch, up to maxFixRounds",
|
|
66
|
+
"(default 2). Kiln does not merge, approve, or comment. Past that bound the",
|
|
67
|
+
"job is needs-human with reason feedback-exhausted.",
|
|
62
68
|
"",
|
|
63
69
|
"serve listens on 127.0.0.1. The page is a board of workflow stages:",
|
|
64
70
|
"queued, spec, build, review, approve, land, PR review, merged, and needs human.",
|
|
65
71
|
"Empty stages collapse. Approve, reject, and retry call the same",
|
|
66
72
|
"functions as the CLI. POST from another origin is rejected. A tick",
|
|
67
73
|
"runs every 30 seconds unless --no-tick is set, using the tick lock.",
|
|
74
|
+
"",
|
|
75
|
+
"sandbox prints the resolved mode, allowlist, and anything missing before a",
|
|
76
|
+
"sandboxed run can start. sandbox build builds the yak agent and proxy images",
|
|
77
|
+
"and installs git into the agent image. Sandbox mode stays off until the kiln",
|
|
78
|
+
"config sets it. A run that fails with yak reason sandbox-error is not retried.",
|
|
68
79
|
].join("\n"));
|
|
69
80
|
process.exit(2);
|
|
70
81
|
}
|
|
@@ -311,6 +322,21 @@ function importCommand(argv) {
|
|
|
311
322
|
if (result.warnings.length > 0)
|
|
312
323
|
process.stderr.write(`${result.warnings.join("\n")}\n`);
|
|
313
324
|
}
|
|
325
|
+
function sandboxCommand(argv) {
|
|
326
|
+
const build = argv[0] === "build";
|
|
327
|
+
const flags = parseFlags(build ? argv.slice(1) : argv);
|
|
328
|
+
rejectServeFlags(flags);
|
|
329
|
+
if (flags.positionals.length !== 0 || flags.test || hasIntakeFlags(flags) || flags.deliver || flags.base || flags.stack) {
|
|
330
|
+
usage();
|
|
331
|
+
}
|
|
332
|
+
if (build) {
|
|
333
|
+
runSandboxBuild();
|
|
334
|
+
process.stdout.write("built yak-agent-sandbox:latest and yak-agent-sandbox-proxy:latest\n");
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const config = resolveKilnConfig({ configPath: flags.configPath });
|
|
338
|
+
process.stdout.write(renderSandboxStatus(config));
|
|
339
|
+
}
|
|
314
340
|
function tickCommand(argv) {
|
|
315
341
|
const flags = parseFlags(argv);
|
|
316
342
|
rejectServeFlags(flags);
|
|
@@ -418,6 +444,10 @@ async function main() {
|
|
|
418
444
|
retryCommand(argv.slice(1));
|
|
419
445
|
return;
|
|
420
446
|
}
|
|
447
|
+
if (command === "sandbox") {
|
|
448
|
+
sandboxCommand(argv.slice(1));
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
421
451
|
if (command === "serve") {
|
|
422
452
|
await serveCommand(argv.slice(1));
|
|
423
453
|
return;
|
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,7 +47,10 @@ 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",
|
|
52
|
+
sandbox: parsed.data.sandbox ?? {},
|
|
53
|
+
workflowSandbox: workflowSandboxFrom(parsed.data.workflows),
|
|
51
54
|
...(jira ? { jira } : {}),
|
|
52
55
|
};
|
|
53
56
|
}
|
|
@@ -234,6 +237,16 @@ function pathToStore(userPath, cwd, home) {
|
|
|
234
237
|
return userPath;
|
|
235
238
|
return resolve(cwd, userPath);
|
|
236
239
|
}
|
|
240
|
+
function workflowSandboxFrom(workflows) {
|
|
241
|
+
if (!workflows)
|
|
242
|
+
return {};
|
|
243
|
+
const out = {};
|
|
244
|
+
for (const [name, entry] of Object.entries(workflows)) {
|
|
245
|
+
if (entry.sandbox)
|
|
246
|
+
out[name] = entry.sandbox;
|
|
247
|
+
}
|
|
248
|
+
return out;
|
|
249
|
+
}
|
|
237
250
|
function jiraFromFile(jira) {
|
|
238
251
|
if (!jira)
|
|
239
252
|
return undefined;
|