@lchase/kiln 0.1.0 → 0.3.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 +51 -15
- package/assets/serve.css +233 -0
- package/dist/cli.js +79 -4
- package/dist/config.js +45 -27
- package/dist/driver.js +203 -12
- package/dist/findings.js +79 -0
- package/dist/intake/jira.js +4 -1
- package/dist/intake/tickets.js +23 -18
- package/dist/jobs.js +6 -1
- package/dist/reconcile.js +19 -1
- package/dist/report.js +168 -71
- package/dist/schemas.js +10 -5
- package/dist/serve-page.js +216 -0
- package/dist/serve.js +351 -0
- package/dist/workflow.js +28 -10
- package/package.json +2 -1
- package/prompts/kiln/investigation.md +31 -0
- package/workflows/investigation.yaml +67 -0
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Kiln
|
|
2
2
|
|
|
3
|
-
Kiln runs a workflow against a git checkout. Register any repo, add a job from a markdown spec, a prompt, a directory of tickets, or a Jira Cloud issue, and tick. Tick launches ready jobs in yak worktrees. Status shows each job. Approve or reject answers a human gate.
|
|
3
|
+
Kiln runs a workflow against a git checkout. Register any repo, add a job from a markdown spec, a prompt, a directory of tickets, or a Jira Cloud issue, and tick. Tick launches ready jobs in yak worktrees. Status shows each job. Approve or reject answers a human gate. `kiln serve` opens that same derived state in a browser on this machine.
|
|
4
4
|
|
|
5
5
|
A ready spec is the input. Kiln does not build or iterate requirements.
|
|
6
6
|
|
|
7
|
-
This repository is public. Checkout paths and jobs stay in
|
|
7
|
+
This repository is public. Checkout paths and jobs stay in `~/.kiln/` (or `$KILN_HOME` when that is set). Do not commit them.
|
|
8
8
|
|
|
9
9
|
## Install
|
|
10
10
|
|
|
@@ -14,6 +14,10 @@ kiln repo add web /absolute/path/to/web --test "npm test"
|
|
|
14
14
|
kiln repo list
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
+
Run `kiln repo add <name> <path>` from any directory. The first run creates `~/.kiln/kiln.config.json`. Jobs, logs, and deliveries live in that same directory, whichever directory you run `kiln` from. Set `KILN_HOME` or pass `--config <file>` to use a different kiln home.
|
|
18
|
+
|
|
19
|
+
If `./.kiln/kiln.config.json` is still in the working directory and the home config does not exist, kiln stops. Move it with `mv ./.kiln ~/.kiln`, or pass `--config` / set `KILN_HOME`. Kiln does not move it for you.
|
|
20
|
+
|
|
17
21
|
`kiln` is the command. The subcommands in the rest of this file are the same ones. Examples that say `npm run kiln --` are the in-checkout form.
|
|
18
22
|
|
|
19
23
|
## Develop from a checkout
|
|
@@ -47,12 +51,12 @@ npm run kiln -- repo add api /absolute/path/to/api --test "npm test"
|
|
|
47
51
|
npm run kiln -- repo list
|
|
48
52
|
```
|
|
49
53
|
|
|
50
|
-
`npm run kiln` builds first. `
|
|
54
|
+
`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.
|
|
51
55
|
|
|
52
56
|
```json
|
|
53
57
|
{
|
|
54
58
|
"repos": {
|
|
55
|
-
"web": { "path": "
|
|
59
|
+
"web": { "path": "/absolute/path/to/web", "test": "npm test" },
|
|
56
60
|
"api": { "path": "~/src/api", "test": "npm test" }
|
|
57
61
|
},
|
|
58
62
|
"maxConcurrent": 2,
|
|
@@ -64,7 +68,7 @@ npm run kiln -- repo list
|
|
|
64
68
|
|
|
65
69
|
## Add a job
|
|
66
70
|
|
|
67
|
-
A job is one workflow run. The workflow name is a file `workflows/<name>.yaml`. `feature` is the factory loop.
|
|
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.
|
|
68
72
|
|
|
69
73
|
```bash
|
|
70
74
|
npm run kiln -- add feature --repo web --spec spec.md --title "Show a Sunday summary"
|
|
@@ -72,7 +76,7 @@ npm run kiln -- add feature --repo api --spec api-spec.md --after <job-id>
|
|
|
72
76
|
npm run kiln -- add feature --repo web --jira PROJ-123
|
|
73
77
|
```
|
|
74
78
|
|
|
75
|
-
`--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
|
|
79
|
+
`--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`:
|
|
76
80
|
|
|
77
81
|
```json
|
|
78
82
|
{
|
|
@@ -87,7 +91,7 @@ npm run kiln -- add feature --repo web --jira PROJ-123
|
|
|
87
91
|
}
|
|
88
92
|
```
|
|
89
93
|
|
|
90
|
-
`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
|
|
94
|
+
`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/`.
|
|
91
95
|
|
|
92
96
|
## Intake
|
|
93
97
|
|
|
@@ -121,7 +125,7 @@ npm run kiln -- import ./tickets --repo web --workflow feature --context spec.md
|
|
|
121
125
|
|
|
122
126
|
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.
|
|
123
127
|
|
|
124
|
-
`--workflow` defaults to `feature`. Kiln switches a ticket to `investigation`
|
|
128
|
+
`--workflow` defaults to `feature`. Kiln switches a ticket to `investigation` when the ticket is a spike: front matter `type` is `spike`, `investigation`, or `research` (the word after a colon counts, as in `wayfinder:research`), the title contains spike or probe, or the text says "no product code". A body that only mentions an upstream spike stays on the requested workflow.
|
|
125
129
|
|
|
126
130
|
`--context` defaults to `spec.md` in the parent of the ticket directory when that file is present. The text is appended to every job spec as a `## Background` section. A context file larger than 40KB is not copied in. The spec names its path instead.
|
|
127
131
|
|
|
@@ -136,7 +140,7 @@ npm run kiln -- add feature --repo web --jira PROJ-123 --after <job-id> --dry-ru
|
|
|
136
140
|
|
|
137
141
|
`--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.
|
|
138
142
|
|
|
139
|
-
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
|
|
143
|
+
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:
|
|
140
144
|
|
|
141
145
|
```json
|
|
142
146
|
"jira": {
|
|
@@ -148,10 +152,30 @@ Set `JIRA_BASE_URL` (for example `https://acme.atlassian.net`), `JIRA_EMAIL`, an
|
|
|
148
152
|
|
|
149
153
|
Create a token at <https://id.atlassian.com/manage-profile/security/api-tokens>. Kiln sends it as Basic auth and does not write it into the job, the spec, or error text. A missing value is an error that names `JIRA_BASE_URL`, `JIRA_EMAIL`, or `JIRA_API_TOKEN`.
|
|
150
154
|
|
|
151
|
-
A Blocks link where this issue is blocked by another key becomes `dependsOn` when a kiln job with `source` `jira` and that key already exists. Otherwise Kiln prints a warning naming the key and still creates the job. `--after` is merged with those ids. Issue type Spike or Research uses the same rule as a spike ticket
|
|
155
|
+
A Blocks link where this issue is blocked by another key becomes `dependsOn` when a kiln job with `source` `jira` and that key already exists. Otherwise Kiln prints a warning naming the key and still creates the job. `--after` is merged with those ids. Issue type Spike or Research uses the same rule as a spike ticket and is imported as `investigation`. Running add again for the same key prints the existing job id and creates nothing. `--dry-run` prints the job and writes nothing.
|
|
152
156
|
|
|
153
157
|
`examples/spec.md` is a fictional spec you can copy.
|
|
154
158
|
|
|
159
|
+
## Serve
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
kiln serve
|
|
163
|
+
kiln serve --port 4100 --tick 30
|
|
164
|
+
kiln serve --no-tick
|
|
165
|
+
```
|
|
166
|
+
|
|
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 and reject functions. Nothing new is stored.
|
|
168
|
+
|
|
169
|
+
`/` is the inbox: jobs in `needs-human`, with the gate prompt, the artifacts and review output yak wrote, links to the spec and the log, and Approve / Reject. An investigation job also links to its findings. `/board` groups every job by state, shows `dependsOn` (including blocked-by), the stacked flag, the pull request for `in-review` and `done`, and the failure or launch error for `failed`. Both pages refresh every 3 seconds.
|
|
170
|
+
|
|
171
|
+
`GET /jobs/<id>/findings` renders the approved findings markdown as text. HTML in that file is escaped. The page is read-only.
|
|
172
|
+
|
|
173
|
+
`GET /api/jobs` and `GET /api/jobs/<id>` return that view as JSON. `POST /api/jobs/<id>/approve` and `POST /api/jobs/<id>/reject` answer the gate. 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).
|
|
174
|
+
|
|
175
|
+
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.
|
|
176
|
+
|
|
177
|
+
The Jira API token is not rendered. Do not commit checkout paths or job files.
|
|
178
|
+
|
|
155
179
|
## Tick, status, approve
|
|
156
180
|
|
|
157
181
|
```bash
|
|
@@ -169,7 +193,7 @@ while true; do npm run kiln -- tick; sleep 30; done
|
|
|
169
193
|
|
|
170
194
|
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.
|
|
171
195
|
|
|
172
|
-
`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 `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`.
|
|
196
|
+
`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 inbox and board link 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`.
|
|
173
197
|
|
|
174
198
|
State is derived from the yak journal, the gate files, and (for `deliver=pr`) `gh pr view`:
|
|
175
199
|
|
|
@@ -184,7 +208,7 @@ State is derived from the yak journal, the gate files, and (for `deliver=pr`) `g
|
|
|
184
208
|
| `failed` | The run failed, the launch died before a journal, the run finished ok with no readable approval, or a pull request could not be opened |
|
|
185
209
|
| `dead` | The journal's process is gone. Status prints the cancel command |
|
|
186
210
|
| `in-review` | `deliver=pr` and the run has landed. The pull request is open, or the next tick still has to open it |
|
|
187
|
-
| `done` | `deliver=local`: the run finished ok and the gate says approve. `deliver=pr`: that pull request has merged |
|
|
211
|
+
| `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` |
|
|
188
212
|
|
|
189
213
|
## What a feature job runs
|
|
190
214
|
|
|
@@ -196,6 +220,18 @@ Verify runs the repo's `test` command with `failOn: never`. Stdout and stderr st
|
|
|
196
220
|
|
|
197
221
|
The first launch adds `.runs/` and `.yak/` to that checkout's `.git/info/exclude`.
|
|
198
222
|
|
|
223
|
+
## What an investigation job runs
|
|
224
|
+
|
|
225
|
+
A spike, probe, or research ticket runs `workflows/investigation.yaml`. One agent explores the worktree. It may run commands and scripts. It must not change product code: the step has no Write or Edit tool, and the prompt forbids creating, editing, deleting, committing, and pushing. The agent returns a markdown document with four sections: Questions, Answers (each answer includes evidence: file paths and command output), Unconfirmed, and Implications for dependent tickets.
|
|
226
|
+
|
|
227
|
+
The run then suspends on a human gate. There is no build, review, verify, or land step. `npm run kiln -- approve <job-id>` stores that markdown at `<kiln-dir>/findings/<job-id>.md` and the job is `done`. Writing the same text again does not change the file. `npm run kiln -- reject <job-id>` is `rejected` and writes no findings file. A rejected investigation does not unblock a dependent.
|
|
228
|
+
|
|
229
|
+
`deliver=pr` does not change this. Kiln does not push the investigation branch and does not run `gh pr create`.
|
|
230
|
+
|
|
231
|
+
A later job whose `dependsOn` includes that investigation receives the findings in the spec yak sees. Kiln appends a `## Findings from <title>` section for each investigation dependency. The copy under `<kiln-dir>/specs/` is left as it was. After merged still waits until the investigation is `done`. `--stack` onto an investigation is an error: there is no branch to target.
|
|
232
|
+
|
|
233
|
+
`adapter: "mock"` reads `test/fixtures/kiln-investigation/explore.json` from the checkout. The prompt is `prompts/kiln/investigation.md`. The published package includes `prompts` and `workflows`.
|
|
234
|
+
|
|
199
235
|
## Delivery and dependencies
|
|
200
236
|
|
|
201
237
|
`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.
|
|
@@ -206,7 +242,7 @@ npm run kiln -- repo add web /absolute/path/to/web --test "npm test" --deliver p
|
|
|
206
242
|
|
|
207
243
|
`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`.
|
|
208
244
|
|
|
209
|
-
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
|
|
245
|
+
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`.
|
|
210
246
|
|
|
211
247
|
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.
|
|
212
248
|
|
|
@@ -216,7 +252,7 @@ If `gh` is missing or not authenticated, that job becomes `failed` and status pr
|
|
|
216
252
|
|
|
217
253
|
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`.
|
|
218
254
|
|
|
219
|
-
`--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. Stacked work does not wait for the upstream pull request to merge.
|
|
255
|
+
`--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.
|
|
220
256
|
|
|
221
257
|
```bash
|
|
222
258
|
npm run kiln -- add feature --repo web --spec spec.md --after <job-id> --stack
|
|
@@ -225,4 +261,4 @@ npm run kiln -- import ./tickets --repo web --stack
|
|
|
225
261
|
|
|
226
262
|
## Not included
|
|
227
263
|
|
|
228
|
-
No requirement refinement. Bug,
|
|
264
|
+
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.
|
package/assets/serve.css
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/* kiln-serve */
|
|
2
|
+
:root {
|
|
3
|
+
color-scheme: light;
|
|
4
|
+
--bg: #efe6d6;
|
|
5
|
+
--bg-2: #e7dcc8;
|
|
6
|
+
--card: #fffaf3;
|
|
7
|
+
--ink: #241c16;
|
|
8
|
+
--muted: #6f6256;
|
|
9
|
+
--line: #e0d3c2;
|
|
10
|
+
--accent: #c2410c;
|
|
11
|
+
--accent-ink: #fff7f2;
|
|
12
|
+
--good: #1f6b45;
|
|
13
|
+
--bad: #9b2c2c;
|
|
14
|
+
--chip: #f3e7d6;
|
|
15
|
+
--shadow: 0 1px 0 rgba(36, 28, 22, 0.04);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
* { box-sizing: border-box; }
|
|
19
|
+
|
|
20
|
+
body {
|
|
21
|
+
margin: 0;
|
|
22
|
+
background:
|
|
23
|
+
radial-gradient(1200px 400px at 10% -10%, #f7efe2, transparent 50%),
|
|
24
|
+
var(--bg);
|
|
25
|
+
color: var(--ink);
|
|
26
|
+
font: 15px/1.45 ui-sans-serif, system-ui, sans-serif;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
a { color: inherit; }
|
|
30
|
+
|
|
31
|
+
.top {
|
|
32
|
+
display: flex;
|
|
33
|
+
align-items: baseline;
|
|
34
|
+
gap: 1.25rem;
|
|
35
|
+
padding: 1rem 1.5rem;
|
|
36
|
+
border-bottom: 1px solid var(--line);
|
|
37
|
+
background: rgba(255, 250, 243, 0.9);
|
|
38
|
+
position: sticky;
|
|
39
|
+
top: 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.kiln-mark {
|
|
43
|
+
font-weight: 700;
|
|
44
|
+
letter-spacing: 0.08em;
|
|
45
|
+
text-transform: uppercase;
|
|
46
|
+
text-decoration: none;
|
|
47
|
+
color: var(--accent);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
nav { display: flex; gap: 0.9rem; }
|
|
51
|
+
|
|
52
|
+
nav a {
|
|
53
|
+
text-decoration: none;
|
|
54
|
+
color: var(--muted);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
nav a[aria-current="page"] {
|
|
58
|
+
color: var(--ink);
|
|
59
|
+
font-weight: 650;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.refresh {
|
|
63
|
+
margin: 0 0 0 auto;
|
|
64
|
+
color: var(--muted);
|
|
65
|
+
font-size: 0.85rem;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
main {
|
|
69
|
+
width: min(980px, calc(100% - 2rem));
|
|
70
|
+
margin: 1.25rem auto 3rem;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
h1 {
|
|
74
|
+
font-size: 1.35rem;
|
|
75
|
+
font-weight: 650;
|
|
76
|
+
margin: 0 0 0.35rem;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.lede {
|
|
80
|
+
margin: 0 0 1.25rem;
|
|
81
|
+
color: var(--muted);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.empty {
|
|
85
|
+
padding: 1.25rem;
|
|
86
|
+
background: var(--card);
|
|
87
|
+
border: 1px solid var(--line);
|
|
88
|
+
border-radius: 12px;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.card, .job {
|
|
92
|
+
background: var(--card);
|
|
93
|
+
border: 1px solid var(--line);
|
|
94
|
+
border-radius: 12px;
|
|
95
|
+
box-shadow: var(--shadow);
|
|
96
|
+
padding: 1rem 1.1rem 1.05rem;
|
|
97
|
+
margin: 0 0 0.85rem;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.card h2, .job h3 {
|
|
101
|
+
margin: 0;
|
|
102
|
+
font-size: 1.15rem;
|
|
103
|
+
font-weight: 650;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.meta, .deps, .links, .why {
|
|
107
|
+
color: var(--muted);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.meta { margin: 0.25rem 0 0.75rem; }
|
|
111
|
+
|
|
112
|
+
.meta code, .deps a {
|
|
113
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
114
|
+
font-size: 0.86em;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.prompt {
|
|
118
|
+
margin: 0 0 0.75rem;
|
|
119
|
+
padding: 0.7rem 0.8rem;
|
|
120
|
+
background: #fff;
|
|
121
|
+
border-left: 3px solid var(--accent);
|
|
122
|
+
white-space: pre-wrap;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.facts {
|
|
126
|
+
list-style: none;
|
|
127
|
+
margin: 0 0 0.75rem;
|
|
128
|
+
padding: 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.facts li { margin: 0.2rem 0; }
|
|
132
|
+
|
|
133
|
+
.facts .name {
|
|
134
|
+
display: inline-block;
|
|
135
|
+
min-width: 5.5rem;
|
|
136
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
137
|
+
font-size: 0.86em;
|
|
138
|
+
color: var(--muted);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
h4 {
|
|
142
|
+
margin: 0.4rem 0 0.2rem;
|
|
143
|
+
font-size: 0.78rem;
|
|
144
|
+
letter-spacing: 0.06em;
|
|
145
|
+
text-transform: uppercase;
|
|
146
|
+
color: var(--muted);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.links a { margin-right: 0.9rem; }
|
|
150
|
+
|
|
151
|
+
.actions { display: flex; gap: 0.5rem; margin-top: 0.85rem; }
|
|
152
|
+
|
|
153
|
+
button {
|
|
154
|
+
font: inherit;
|
|
155
|
+
border-radius: 8px;
|
|
156
|
+
padding: 0.4rem 0.85rem;
|
|
157
|
+
cursor: pointer;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
button.approve {
|
|
161
|
+
background: var(--accent);
|
|
162
|
+
color: var(--accent-ink);
|
|
163
|
+
border: 1px solid var(--accent);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
button.reject {
|
|
167
|
+
background: transparent;
|
|
168
|
+
color: var(--ink);
|
|
169
|
+
border: 1px solid var(--line);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.group { margin: 0 0 1.4rem; }
|
|
173
|
+
|
|
174
|
+
.group h2 {
|
|
175
|
+
font-size: 0.95rem;
|
|
176
|
+
margin: 0 0 0.45rem;
|
|
177
|
+
display: flex;
|
|
178
|
+
gap: 0.5rem;
|
|
179
|
+
align-items: baseline;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.count {
|
|
183
|
+
color: var(--muted);
|
|
184
|
+
font-weight: 450;
|
|
185
|
+
font-size: 0.85rem;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.chips { display: flex; flex-wrap: wrap; gap: 0.35rem; margin: 0.45rem 0; }
|
|
189
|
+
|
|
190
|
+
.chip {
|
|
191
|
+
background: var(--chip);
|
|
192
|
+
border-radius: 999px;
|
|
193
|
+
padding: 0.1rem 0.5rem;
|
|
194
|
+
font-size: 0.78rem;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.chip.state-needs-human,
|
|
198
|
+
.chip.state-blocked { background: #f6d7b8; }
|
|
199
|
+
|
|
200
|
+
.chip.state-failed,
|
|
201
|
+
.chip.state-rejected,
|
|
202
|
+
.chip.state-dead,
|
|
203
|
+
.chip.state-invalid { background: #f3d0d0; color: var(--bad); }
|
|
204
|
+
|
|
205
|
+
.chip.state-approved,
|
|
206
|
+
.chip.state-done,
|
|
207
|
+
.chip.state-in-review { background: #d7eadf; color: var(--good); }
|
|
208
|
+
|
|
209
|
+
.chip.state-running,
|
|
210
|
+
.chip.state-queued { background: #d9e4f2; }
|
|
211
|
+
|
|
212
|
+
.deps { margin: 0.35rem 0; }
|
|
213
|
+
|
|
214
|
+
.details {
|
|
215
|
+
margin: 0.4rem 0 0;
|
|
216
|
+
padding: 0;
|
|
217
|
+
list-style: none;
|
|
218
|
+
color: var(--muted);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.details li.bad { color: var(--bad); }
|
|
222
|
+
|
|
223
|
+
pre.text {
|
|
224
|
+
white-space: pre-wrap;
|
|
225
|
+
word-break: break-word;
|
|
226
|
+
background: var(--card);
|
|
227
|
+
border: 1px solid var(--line);
|
|
228
|
+
border-radius: 12px;
|
|
229
|
+
padding: 1rem;
|
|
230
|
+
margin: 0;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.back { display: inline-block; margin-bottom: 0.75rem; }
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { addRepo, renderRepoList, resolveKilnConfig } from "./config.js";
|
|
3
3
|
import { answerJob, runStatus, runTick } from "./driver.js";
|
|
4
|
+
import { startServe } from "./serve.js";
|
|
4
5
|
import { addFromFile } from "./intake/file.js";
|
|
5
6
|
import { addFromJira } from "./intake/jira.js";
|
|
6
7
|
import { addFromPrompt } from "./intake/prompt.js";
|
|
@@ -16,15 +17,24 @@ function usage() {
|
|
|
16
17
|
" kiln status [jobId] [--config <file>]",
|
|
17
18
|
" kiln approve <jobId> [--config <file>]",
|
|
18
19
|
" kiln reject <jobId> [--config <file>]",
|
|
20
|
+
" kiln serve [--port 4100] [--tick <seconds>|--no-tick] [--config <file>]",
|
|
19
21
|
"",
|
|
20
|
-
"repo add registers a checkout.
|
|
21
|
-
"
|
|
22
|
+
"repo add registers a checkout. A relative path is stored absolute, resolved",
|
|
23
|
+
"from the working directory. ~ is expanded from the home directory.",
|
|
24
|
+
"",
|
|
25
|
+
"Config, jobs, logs, and deliveries live in one kiln home: --config <file>",
|
|
26
|
+
"if given, otherwise $KILN_HOME/kiln.config.json when KILN_HOME is set,",
|
|
27
|
+
"otherwise ~/.kiln/kiln.config.json. repo add creates that directory.",
|
|
28
|
+
"Run kiln repo add <name> <path> from any directory. A leftover",
|
|
29
|
+
"./.kiln/kiln.config.json is an error until you mv ./.kiln ~/.kiln,",
|
|
30
|
+
"pass --config <file>, or set KILN_HOME.",
|
|
22
31
|
"",
|
|
23
32
|
"add writes one job from a spec file, from --prompt text (--prompt - reads stdin),",
|
|
24
33
|
"or from a Jira Cloud issue (--jira PROJ-123). Pass one of those, not more.",
|
|
25
34
|
"The repo and the workflow (workflows/<name>.yaml) must already exist.",
|
|
26
35
|
"--after names jobs that must be done before this one launches. --stack waits",
|
|
27
36
|
"for one upstream to land and opens the pull request against that branch.",
|
|
37
|
+
"--stack cannot target an investigation job: that workflow does not land a branch.",
|
|
28
38
|
"A Jira Blocks link where this issue is blocked by another key is included",
|
|
29
39
|
"when a kiln job for that key already exists.",
|
|
30
40
|
"--dry-run with --jira prints the job and writes nothing. Running add again",
|
|
@@ -36,13 +46,20 @@ function usage() {
|
|
|
36
46
|
"",
|
|
37
47
|
"tick launches at most one ready job. After merged, dependencies must be done.",
|
|
38
48
|
"Stacked jobs launch once the upstream has landed. deliver=pr then pushes",
|
|
39
|
-
"yak/<run-id> and opens a draft pull request.
|
|
49
|
+
"yak/<run-id> and opens a draft pull request. An investigation is done when",
|
|
50
|
+
"its gate is approved. It does not open a pull request. status prints the",
|
|
51
|
+
"derived state, and the findings path once an investigation is approved.",
|
|
40
52
|
"approve and reject answer the human gate.",
|
|
53
|
+
"",
|
|
54
|
+
"serve listens on 127.0.0.1. The inbox lists needs-human jobs. The board",
|
|
55
|
+
"groups every job by the same derived state as status. Approve and reject",
|
|
56
|
+
"call the same functions as the CLI. POST from another origin is rejected.",
|
|
57
|
+
"A tick runs every 30 seconds unless --no-tick is set, using the tick lock.",
|
|
41
58
|
].join("\n"));
|
|
42
59
|
process.exit(2);
|
|
43
60
|
}
|
|
44
61
|
function parseFlags(argv) {
|
|
45
|
-
const bag = { positionals: [], after: [], dryRun: false, stack: false };
|
|
62
|
+
const bag = { positionals: [], after: [], dryRun: false, stack: false, noTick: false };
|
|
46
63
|
for (let i = 0; i < argv.length; i += 1) {
|
|
47
64
|
const flag = argv[i];
|
|
48
65
|
const next = argv[i + 1];
|
|
@@ -138,6 +155,30 @@ function parseFlags(argv) {
|
|
|
138
155
|
i += 1;
|
|
139
156
|
continue;
|
|
140
157
|
}
|
|
158
|
+
if (flag === "--port") {
|
|
159
|
+
if (!next || !/^\d+$/.test(next))
|
|
160
|
+
usage();
|
|
161
|
+
const port = Number(next);
|
|
162
|
+
if (port > 65535)
|
|
163
|
+
usage();
|
|
164
|
+
bag.port = port;
|
|
165
|
+
i += 1;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (flag === "--tick") {
|
|
169
|
+
if (!next || !/^[1-9]\d*$/.test(next))
|
|
170
|
+
usage();
|
|
171
|
+
const seconds = Number(next);
|
|
172
|
+
if (seconds > 86_400)
|
|
173
|
+
usage();
|
|
174
|
+
bag.tickSeconds = seconds;
|
|
175
|
+
i += 1;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (flag === "--no-tick") {
|
|
179
|
+
bag.noTick = true;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
141
182
|
if (flag?.startsWith("--"))
|
|
142
183
|
usage();
|
|
143
184
|
if (flag !== undefined)
|
|
@@ -145,9 +186,14 @@ function parseFlags(argv) {
|
|
|
145
186
|
}
|
|
146
187
|
return bag;
|
|
147
188
|
}
|
|
189
|
+
function rejectServeFlags(flags) {
|
|
190
|
+
if (flags.port !== undefined || flags.tickSeconds !== undefined || flags.noTick)
|
|
191
|
+
usage();
|
|
192
|
+
}
|
|
148
193
|
function repoCommand(argv) {
|
|
149
194
|
const sub = argv[0];
|
|
150
195
|
const flags = parseFlags(argv.slice(1));
|
|
196
|
+
rejectServeFlags(flags);
|
|
151
197
|
if (sub === "list") {
|
|
152
198
|
if (flags.positionals.length !== 0 || hasIntakeFlags(flags) || flags.test || flags.deliver || flags.base)
|
|
153
199
|
usage();
|
|
@@ -174,6 +220,7 @@ function repoCommand(argv) {
|
|
|
174
220
|
}
|
|
175
221
|
async function addCommand(argv) {
|
|
176
222
|
const flags = parseFlags(argv);
|
|
223
|
+
rejectServeFlags(flags);
|
|
177
224
|
const workflow = flags.positionals[0];
|
|
178
225
|
const selected = [flags.spec !== undefined, flags.prompt !== undefined, flags.jira !== undefined].filter(Boolean).length;
|
|
179
226
|
if (!workflow ||
|
|
@@ -226,6 +273,7 @@ async function addCommand(argv) {
|
|
|
226
273
|
}
|
|
227
274
|
function importCommand(argv) {
|
|
228
275
|
const flags = parseFlags(argv);
|
|
276
|
+
rejectServeFlags(flags);
|
|
229
277
|
const dir = flags.positionals[0];
|
|
230
278
|
if (!dir ||
|
|
231
279
|
flags.positionals.length !== 1 ||
|
|
@@ -255,6 +303,7 @@ function importCommand(argv) {
|
|
|
255
303
|
}
|
|
256
304
|
function tickCommand(argv) {
|
|
257
305
|
const flags = parseFlags(argv);
|
|
306
|
+
rejectServeFlags(flags);
|
|
258
307
|
if (flags.positionals.length !== 0 || flags.test || hasIntakeFlags(flags) || flags.deliver || flags.base || flags.stack) {
|
|
259
308
|
usage();
|
|
260
309
|
}
|
|
@@ -263,6 +312,7 @@ function tickCommand(argv) {
|
|
|
263
312
|
}
|
|
264
313
|
function statusCommand(argv) {
|
|
265
314
|
const flags = parseFlags(argv);
|
|
315
|
+
rejectServeFlags(flags);
|
|
266
316
|
if (flags.positionals.length > 1 || flags.test || hasIntakeFlags(flags) || flags.deliver || flags.base || flags.stack) {
|
|
267
317
|
usage();
|
|
268
318
|
}
|
|
@@ -271,6 +321,7 @@ function statusCommand(argv) {
|
|
|
271
321
|
}
|
|
272
322
|
function gateCommand(choice, argv) {
|
|
273
323
|
const flags = parseFlags(argv);
|
|
324
|
+
rejectServeFlags(flags);
|
|
274
325
|
const jobId = flags.positionals[0];
|
|
275
326
|
if (!jobId || flags.positionals.length !== 1 || flags.test || hasIntakeFlags(flags) || flags.deliver || flags.base || flags.stack) {
|
|
276
327
|
usage();
|
|
@@ -278,6 +329,26 @@ function gateCommand(choice, argv) {
|
|
|
278
329
|
const config = resolveKilnConfig({ configPath: flags.configPath });
|
|
279
330
|
process.stdout.write(answerJob(config, jobId, choice));
|
|
280
331
|
}
|
|
332
|
+
async function serveCommand(argv) {
|
|
333
|
+
const flags = parseFlags(argv);
|
|
334
|
+
if (flags.positionals.length !== 0 ||
|
|
335
|
+
flags.test ||
|
|
336
|
+
hasIntakeFlags(flags) ||
|
|
337
|
+
flags.deliver ||
|
|
338
|
+
flags.base ||
|
|
339
|
+
flags.stack ||
|
|
340
|
+
(flags.noTick && flags.tickSeconds !== undefined)) {
|
|
341
|
+
usage();
|
|
342
|
+
}
|
|
343
|
+
const config = resolveKilnConfig({ configPath: flags.configPath });
|
|
344
|
+
const port = flags.port ?? 4100;
|
|
345
|
+
const tickSeconds = flags.noTick ? undefined : (flags.tickSeconds ?? 30);
|
|
346
|
+
await startServe({
|
|
347
|
+
config,
|
|
348
|
+
port,
|
|
349
|
+
...(tickSeconds !== undefined ? { tickSeconds } : {}),
|
|
350
|
+
});
|
|
351
|
+
}
|
|
281
352
|
function hasIntakeFlags(flags) {
|
|
282
353
|
return Boolean(flags.repo ||
|
|
283
354
|
flags.spec ||
|
|
@@ -323,6 +394,10 @@ async function main() {
|
|
|
323
394
|
gateCommand("reject", argv.slice(1));
|
|
324
395
|
return;
|
|
325
396
|
}
|
|
397
|
+
if (command === "serve") {
|
|
398
|
+
await serveCommand(argv.slice(1));
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
326
401
|
usage();
|
|
327
402
|
}
|
|
328
403
|
try {
|