@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 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 gitignored `.kiln/` or `~/.kiln/`. Do not commit them.
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. `npm run kiln -- repo add` writes `./.kiln/kiln.config.json` (or updates `~/.kiln/kiln.config.json` when that file already exists). A relative path is stored relative to the config file's directory. `~` is expanded from the home directory. `test` is optional on the entry and required before tick can launch a job in that repo.
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": "../web", "test": "npm test" },
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. Bug, investigation, and security workflows are next. A later `pr-review` workflow is not in this repo yet.
71
+ A job is one workflow run. The workflow name is a file `workflows/<name>.yaml`. `feature` is the factory loop. `investigation` answers a spike and stops for a person to approve the findings. 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 `.kiln/specs/<id>.md` and writes `.kiln/jobs/<id>.json`:
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 `.kiln/deliveries/`.
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` only when `workflows/investigation.yaml` exists and 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. `investigation` is not in this repo, so a spike ticket stays on the requested workflow and Kiln prints a warning.
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 gitignored kiln config (`./.kiln/kiln.config.json`, or `~/.kiln/kiln.config.json` when the working directory has no config):
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: `investigation` when `workflows/investigation.yaml` exists, and a warning on the requested workflow when it does not. Running add again for the same key prints the existing job id and creates nothing. `--dry-run` prints the job and writes nothing.
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 `.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.
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, investigation, security, and pr-review workflows are not in this repo yet. Kiln does not merge pull requests and does not push the base branch.
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.
@@ -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. Relative paths are stored relative to the",
21
- "config directory. ~ is expanded from the home directory.",
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. status prints the derived state.",
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 {