@lchase/kiln 0.0.0-stage → 0.1.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,3 +1,228 @@
1
- # Temporary Holding Version
1
+ # Kiln
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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.
4
+
5
+ A ready spec is the input. Kiln does not build or iterate requirements.
6
+
7
+ This repository is public. Checkout paths and jobs stay in gitignored `.kiln/` or `~/.kiln/`. Do not commit them.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm i -g @lchase/kiln
13
+ kiln repo add web /absolute/path/to/web --test "npm test"
14
+ kiln repo list
15
+ ```
16
+
17
+ `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
+
19
+ ## Develop from a checkout
20
+
21
+ ```bash
22
+ npm install
23
+ npm run build
24
+ npm link
25
+ kiln repo list
26
+ ```
27
+
28
+ `npm link` puts this checkout's `kiln` on your PATH. The bin is `dist/cli.js`, so build before linking, and rebuild after source changes. `npm run kiln --` builds and runs without a global link.
29
+
30
+ Workflows (`workflows/*.yaml`) and prompts (`prompts/`) are read from the package that contains the running file, next to its `package.json`. A global install and `npm link` both resolve them from that package, not from the directory you ran `kiln` in.
31
+
32
+ ## Prerequisites
33
+
34
+ - Node.js >= 22 (the `engines` field in `package.json`)
35
+ - git
36
+ - Claude Code, installed and logged in, for a real run. `adapter: "mock"` does not call a model.
37
+ - `gh`, installed and logged in, only when a repo uses `deliver: "pr"`.
38
+
39
+ 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).
40
+
41
+ ## Register repos
42
+
43
+ ```bash
44
+ npm install
45
+ npm run kiln -- repo add web /absolute/path/to/web --test "npm test"
46
+ npm run kiln -- repo add api /absolute/path/to/api --test "npm test"
47
+ npm run kiln -- repo list
48
+ ```
49
+
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.
51
+
52
+ ```json
53
+ {
54
+ "repos": {
55
+ "web": { "path": "../web", "test": "npm test" },
56
+ "api": { "path": "~/src/api", "test": "npm test" }
57
+ },
58
+ "maxConcurrent": 2,
59
+ "adapter": "claude-code"
60
+ }
61
+ ```
62
+
63
+ `repos` is an open map. Any name is allowed. `maxConcurrent` caps how many jobs may be in flight (default 2). `adapter` is `claude-code` (default) or `mock`.
64
+
65
+ ## Add a job
66
+
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.
68
+
69
+ ```bash
70
+ npm run kiln -- add feature --repo web --spec spec.md --title "Show a Sunday summary"
71
+ npm run kiln -- add feature --repo api --spec api-spec.md --after <job-id>
72
+ npm run kiln -- add feature --repo web --jira PROJ-123
73
+ ```
74
+
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`:
76
+
77
+ ```json
78
+ {
79
+ "id": "j20261025020600ab12",
80
+ "repo": "web",
81
+ "workflow": "feature",
82
+ "spec": "specs/j20261025020600ab12.md",
83
+ "title": "Show a Sunday summary",
84
+ "dependsOn": ["j20261025020500cd34"],
85
+ "source": { "kind": "file", "ref": "/home/dev/specs/sunday.md" },
86
+ "createdAt": "2026-10-25T02:06:00.000Z"
87
+ }
88
+ ```
89
+
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/`.
91
+
92
+ ## Intake
93
+
94
+ Every intake writes the same job: `id`, `repo`, `workflow`, `spec`, `title`, optional `dependsOn`, and `source`. `source` is `{ "kind": "file" | "prompt" | "tickets" | "jira", "ref": "..." }`. Status prints `source <kind> <ref>` when the job has one. Tick, reconcile, and status do not special-case an intake. The adapters are `src/intake/file.ts`, `src/intake/prompt.ts`, `src/intake/tickets.ts`, and `src/intake/jira.ts`.
95
+
96
+ ### File
97
+
98
+ ```bash
99
+ npm run kiln -- add feature --repo web --spec spec.md --title "Show a Sunday summary"
100
+ ```
101
+
102
+ This copies the markdown file into the kiln directory. `source.kind` is `file`. `source.ref` is the absolute path of the file that was copied. `--title` defaults to the first `#` heading, then the file name. `--after` may be repeated, and those jobs must already exist.
103
+
104
+ ### Prompt
105
+
106
+ ```bash
107
+ npm run kiln -- add feature --repo web --prompt "Show a quiet empty state"
108
+ npm run kiln -- add feature --repo api --prompt - < notes.md
109
+ ```
110
+
111
+ `--prompt -` reads stdin. The text is stored as that job's spec. The title defaults to the first non-empty line, truncated to 80 characters. `source.kind` is `prompt`. `source.ref` is `inline` for an argument and `stdin` when the prompt is read from stdin. `--title` and `--after` work the same way as file intake. Pass one of `--spec`, `--prompt`, or `--jira`.
112
+
113
+ ### Tickets
114
+
115
+ ```bash
116
+ npm run kiln -- import ./tickets --repo web
117
+ npm run kiln -- import ./tickets --repo web --workflow feature --context spec.md --dry-run
118
+ ```
119
+
120
+ `import` reads a directory of `NN-slug.md` files, the shape a Matt Pocock-style `/to-tickets` pass writes. Each file starts with `# NN: Title`, a `**What to build:**` paragraph, and a `**Blocked by:**` line. That line is followed by `None…` or by bullets such as `- 01: Probe the unconfirmed platform facts`. Kiln matches the `NN` number to a filename in the same directory (`01` and `1` are the same number). YAML front matter is also accepted, with `title`, `type`, and `blocked-by` (`blocked-by:` as a list, a flow list, or a single number).
121
+
122
+ 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
+
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.
125
+
126
+ `--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
+
128
+ Import is idempotent. The key is the ticket directory plus the filename, stored as `source.ref` (an absolute path) with `source.kind` `tickets`. Running import again on the same directory does not create a second job for a file already imported. The report says how many jobs are new and how many are unchanged. `--dry-run` prints each ticket, its workflow, and its dependencies, and writes nothing.
129
+
130
+ ### Jira
131
+
132
+ ```bash
133
+ npm run kiln -- add feature --repo web --jira PROJ-123
134
+ npm run kiln -- add feature --repo web --jira PROJ-123 --after <job-id> --dry-run
135
+ ```
136
+
137
+ `--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
+
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):
140
+
141
+ ```json
142
+ "jira": {
143
+ "baseUrl": "https://acme.atlassian.net",
144
+ "email": "dev@example.com",
145
+ "apiToken": "<token>"
146
+ }
147
+ ```
148
+
149
+ 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
+
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.
152
+
153
+ `examples/spec.md` is a fictional spec you can copy.
154
+
155
+ ## Tick, status, approve
156
+
157
+ ```bash
158
+ npm run kiln -- tick
159
+ npm run kiln -- status
160
+ npm run kiln -- approve <job-id>
161
+ npm run kiln -- reject <job-id>
162
+ ```
163
+
164
+ `tick` launches at most one ready job and returns. Run it again until nothing is left to launch:
165
+
166
+ ```bash
167
+ while true; do npm run kiln -- tick; sleep 30; done
168
+ ```
169
+
170
+ 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
+
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`.
173
+
174
+ State is derived from the yak journal, the gate files, and (for `deliver=pr`) `gh pr view`:
175
+
176
+ | State | Meaning |
177
+ | --- | --- |
178
+ | `queued` | Dependencies are met, and the job has not started |
179
+ | `blocked` | An after-merged dependency is not `done`, or a stacked dependency has not landed. A missing job id is named |
180
+ | `running` | Yak is alive, or the launch process is alive and has not written a journal |
181
+ | `needs-human` | The run is suspended on an open gate |
182
+ | `approved` | The gate says approve and the run has not finished. This still holds a slot |
183
+ | `rejected` | The run finished ok and the gate says reject |
184
+ | `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
+ | `dead` | The journal's process is gone. Status prints the cancel command |
186
+ | `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 |
188
+
189
+ ## What a feature job runs
190
+
191
+ `npm run kiln -- tick` starts `yak run --isolation worktree --from <sha>` in the repo, tagged `kiln:<jobId>`, with logs in `<kiln-dir>/logs/<jobId>.log`. Kiln passes `title`, the spec text as `description` and `acceptance`, `baseRef` (the commit the worktree is cut from), and `runNonce`. For `deliver=local` that commit is checkout `HEAD`. For `deliver=pr` it is `origin/<base>` after `git fetch`, or the upstream branch tip when the job is stacked. The test command is `KILN_TEST`. `--from` is that same commit. Yak cuts the worktree from it and leaves the checkout's `HEAD` and files where they are.
192
+
193
+ The job runs the factory loop from Lawrence Chase's factory plugin v0.7.0 (MIT), copied from commit `4c84a2ce7fbde5ac298c0de9f0b90141eac4ed98`: spec, then up to two rounds of build, verify, and the security, ux, ui, and code reviewers, then the approver, a human gate, and a commit. The prompts in `prompts/factory/` are those agent files, copied verbatim. `prompts/kiln/` maps the factory's job-folder files onto the artifacts yak passes in. `npm run kiln -- tick` writes a per-run copy of the workflow under the kiln directory, with absolute prompt paths, before it starts yak.
194
+
195
+ Verify runs the repo's `test` command with `failOn: never`. Stdout and stderr stored from verify are cut to the last 200 lines or 20KB. One round is build, then verify, then the four reviewers. `concurrency: 4` starts security, ux, ui, and code together after verify. Build stays first. The approver runs after the loop. Each reviewer needs `spec`, `build`, and `verify`, and inherits `input` so the preamble can still read `baseRef`. Yak warns that those four Bash steps share one working tree and may race. That warning is expected: the reviewer preambles say to use the verify output and not re-run tests, builds, or anything else that writes. Git diff and file reads stay allowed. A `CHANGES` verdict, a `BLOCKED` build, or a non-zero verify starts another build, at most twice. The loop then continues to the approver. After an approved gate, the run commits on `yak/<run-id>` with message `kiln: <title>`. A reject skips that commit. `adapter: "mock"` reads fixtures at `test/fixtures/kiln-feature/<step>.json` and does not call a model.
196
+
197
+ The first launch adds `.runs/` and `.yak/` to that checkout's `.git/info/exclude`.
198
+
199
+ ## Delivery and dependencies
200
+
201
+ `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.
202
+
203
+ ```bash
204
+ npm run kiln -- repo add web /absolute/path/to/web --test "npm test" --deliver pr --base main
205
+ ```
206
+
207
+ `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
+
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.
210
+
211
+ 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
+
213
+ `done` for `deliver=pr` is `gh pr view --json state,mergedAt`. A squash merge counts. Kiln does not ask git whether the landed commit is an ancestor of the base.
214
+
215
+ ### After merged and stacked
216
+
217
+ 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
+
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.
220
+
221
+ ```bash
222
+ npm run kiln -- add feature --repo web --spec spec.md --after <job-id> --stack
223
+ npm run kiln -- import ./tickets --repo web --stack
224
+ ```
225
+
226
+ ## Not included
227
+
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.
package/dist/cli.js ADDED
@@ -0,0 +1,334 @@
1
+ #!/usr/bin/env node
2
+ import { addRepo, renderRepoList, resolveKilnConfig } from "./config.js";
3
+ import { answerJob, runStatus, runTick } from "./driver.js";
4
+ import { addFromFile } from "./intake/file.js";
5
+ import { addFromJira } from "./intake/jira.js";
6
+ import { addFromPrompt } from "./intake/prompt.js";
7
+ import { importTickets } from "./intake/tickets.js";
8
+ import { renderAddedJob } from "./jobs.js";
9
+ function usage() {
10
+ console.error([
11
+ "Usage: kiln repo add <name> <path> [--test <cmd>] [--deliver local|pr] [--base <branch>] [--config <file>]",
12
+ " kiln repo list [--config <file>]",
13
+ " kiln add <workflow> --repo <name> (--spec <file.md> | --prompt <text> | --jira <KEY>) [--title <text>] [--after <jobId>]... [--stack] [--dry-run] [--config <file>]",
14
+ " kiln import <dir> --repo <name> [--workflow <name>] [--context <file.md>] [--stack] [--dry-run] [--config <file>]",
15
+ " kiln tick [--config <file>]",
16
+ " kiln status [jobId] [--config <file>]",
17
+ " kiln approve <jobId> [--config <file>]",
18
+ " kiln reject <jobId> [--config <file>]",
19
+ "",
20
+ "repo add registers a checkout. Relative paths are stored relative to the",
21
+ "config directory. ~ is expanded from the home directory.",
22
+ "",
23
+ "add writes one job from a spec file, from --prompt text (--prompt - reads stdin),",
24
+ "or from a Jira Cloud issue (--jira PROJ-123). Pass one of those, not more.",
25
+ "The repo and the workflow (workflows/<name>.yaml) must already exist.",
26
+ "--after names jobs that must be done before this one launches. --stack waits",
27
+ "for one upstream to land and opens the pull request against that branch.",
28
+ "A Jira Blocks link where this issue is blocked by another key is included",
29
+ "when a kiln job for that key already exists.",
30
+ "--dry-run with --jira prints the job and writes nothing. Running add again",
31
+ "for the same Jira key prints the existing job id and creates nothing.",
32
+ "",
33
+ "import reads a directory of NN-slug.md tickets and writes one job per file.",
34
+ "Blocked-by becomes dependsOn. Re-running the same directory does not duplicate jobs.",
35
+ "--dry-run prints the jobs and writes nothing.",
36
+ "",
37
+ "tick launches at most one ready job. After merged, dependencies must be done.",
38
+ "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.",
40
+ "approve and reject answer the human gate.",
41
+ ].join("\n"));
42
+ process.exit(2);
43
+ }
44
+ function parseFlags(argv) {
45
+ const bag = { positionals: [], after: [], dryRun: false, stack: false };
46
+ for (let i = 0; i < argv.length; i += 1) {
47
+ const flag = argv[i];
48
+ const next = argv[i + 1];
49
+ if (flag === "--config") {
50
+ if (!next || next.startsWith("--"))
51
+ usage();
52
+ bag.configPath = next;
53
+ i += 1;
54
+ continue;
55
+ }
56
+ if (flag === "--test") {
57
+ if (!next || next.startsWith("--"))
58
+ usage();
59
+ bag.test = next;
60
+ i += 1;
61
+ continue;
62
+ }
63
+ if (flag === "--repo") {
64
+ if (!next || next.startsWith("--"))
65
+ usage();
66
+ bag.repo = next;
67
+ i += 1;
68
+ continue;
69
+ }
70
+ if (flag === "--spec") {
71
+ if (!next || next.startsWith("--"))
72
+ usage();
73
+ bag.spec = next;
74
+ i += 1;
75
+ continue;
76
+ }
77
+ if (flag === "--prompt") {
78
+ if (!next || next.startsWith("--"))
79
+ usage();
80
+ bag.prompt = next;
81
+ i += 1;
82
+ continue;
83
+ }
84
+ if (flag === "--jira") {
85
+ if (!next || next.startsWith("--"))
86
+ usage();
87
+ bag.jira = next;
88
+ i += 1;
89
+ continue;
90
+ }
91
+ if (flag === "--workflow") {
92
+ if (!next || next.startsWith("--"))
93
+ usage();
94
+ bag.workflow = next;
95
+ i += 1;
96
+ continue;
97
+ }
98
+ if (flag === "--context") {
99
+ if (!next || next.startsWith("--"))
100
+ usage();
101
+ bag.context = next;
102
+ i += 1;
103
+ continue;
104
+ }
105
+ if (flag === "--dry-run") {
106
+ bag.dryRun = true;
107
+ continue;
108
+ }
109
+ if (flag === "--title") {
110
+ if (!next || next.startsWith("--"))
111
+ usage();
112
+ bag.title = next;
113
+ i += 1;
114
+ continue;
115
+ }
116
+ if (flag === "--after") {
117
+ if (!next || next.startsWith("--"))
118
+ usage();
119
+ bag.after.push(next);
120
+ i += 1;
121
+ continue;
122
+ }
123
+ if (flag === "--stack") {
124
+ bag.stack = true;
125
+ continue;
126
+ }
127
+ if (flag === "--deliver") {
128
+ if (next !== "local" && next !== "pr")
129
+ usage();
130
+ bag.deliver = next;
131
+ i += 1;
132
+ continue;
133
+ }
134
+ if (flag === "--base") {
135
+ if (!next || next.startsWith("--"))
136
+ usage();
137
+ bag.base = next;
138
+ i += 1;
139
+ continue;
140
+ }
141
+ if (flag?.startsWith("--"))
142
+ usage();
143
+ if (flag !== undefined)
144
+ bag.positionals.push(flag);
145
+ }
146
+ return bag;
147
+ }
148
+ function repoCommand(argv) {
149
+ const sub = argv[0];
150
+ const flags = parseFlags(argv.slice(1));
151
+ if (sub === "list") {
152
+ if (flags.positionals.length !== 0 || hasIntakeFlags(flags) || flags.test || flags.deliver || flags.base)
153
+ usage();
154
+ const config = resolveKilnConfig({ configPath: flags.configPath });
155
+ process.stdout.write(renderRepoList(config));
156
+ return;
157
+ }
158
+ if (sub === "add") {
159
+ const name = flags.positionals[0];
160
+ const repoPath = flags.positionals[1];
161
+ if (!name || !repoPath || flags.positionals.length !== 2 || hasIntakeFlags(flags))
162
+ usage();
163
+ process.stdout.write(addRepo({
164
+ configPath: flags.configPath,
165
+ name,
166
+ repoPath,
167
+ ...(flags.test !== undefined ? { test: flags.test } : {}),
168
+ ...(flags.deliver !== undefined ? { deliver: flags.deliver } : {}),
169
+ ...(flags.base !== undefined ? { base: flags.base } : {}),
170
+ }));
171
+ return;
172
+ }
173
+ usage();
174
+ }
175
+ async function addCommand(argv) {
176
+ const flags = parseFlags(argv);
177
+ const workflow = flags.positionals[0];
178
+ const selected = [flags.spec !== undefined, flags.prompt !== undefined, flags.jira !== undefined].filter(Boolean).length;
179
+ if (!workflow ||
180
+ flags.positionals.length !== 1 ||
181
+ !flags.repo ||
182
+ selected !== 1 ||
183
+ flags.test ||
184
+ flags.workflow ||
185
+ flags.context ||
186
+ flags.deliver ||
187
+ flags.base ||
188
+ (flags.dryRun && flags.jira === undefined)) {
189
+ usage();
190
+ }
191
+ const config = resolveKilnConfig({ configPath: flags.configPath });
192
+ if (flags.jira !== undefined) {
193
+ const result = await addFromJira(config, {
194
+ workflow,
195
+ repo: flags.repo,
196
+ key: flags.jira,
197
+ ...(flags.title !== undefined ? { title: flags.title } : {}),
198
+ dependsOn: flags.after,
199
+ ...(flags.stack ? { stack: true } : {}),
200
+ dryRun: flags.dryRun,
201
+ });
202
+ process.stdout.write(result.report);
203
+ if (result.warnings.length > 0)
204
+ process.stderr.write(`${result.warnings.join("\n")}\n`);
205
+ return;
206
+ }
207
+ const { job, jsonPath } = flags.spec !== undefined
208
+ ? addFromFile(config, {
209
+ workflow,
210
+ repo: flags.repo,
211
+ specPath: flags.spec,
212
+ ...(flags.title !== undefined ? { title: flags.title } : {}),
213
+ dependsOn: flags.after,
214
+ ...(flags.stack ? { stack: true } : {}),
215
+ cwd: process.cwd(),
216
+ })
217
+ : addFromPrompt(config, {
218
+ workflow,
219
+ repo: flags.repo,
220
+ prompt: flags.prompt ?? "",
221
+ ...(flags.title !== undefined ? { title: flags.title } : {}),
222
+ dependsOn: flags.after,
223
+ ...(flags.stack ? { stack: true } : {}),
224
+ });
225
+ process.stdout.write(renderAddedJob(job, jsonPath));
226
+ }
227
+ function importCommand(argv) {
228
+ const flags = parseFlags(argv);
229
+ const dir = flags.positionals[0];
230
+ if (!dir ||
231
+ flags.positionals.length !== 1 ||
232
+ !flags.repo ||
233
+ flags.spec ||
234
+ flags.prompt ||
235
+ flags.title ||
236
+ flags.after.length > 0 ||
237
+ flags.test ||
238
+ flags.deliver ||
239
+ flags.base) {
240
+ usage();
241
+ }
242
+ const config = resolveKilnConfig({ configPath: flags.configPath });
243
+ const result = importTickets(config, {
244
+ dir,
245
+ repo: flags.repo,
246
+ ...(flags.workflow !== undefined ? { workflow: flags.workflow } : {}),
247
+ ...(flags.context !== undefined ? { contextPath: flags.context } : {}),
248
+ dryRun: flags.dryRun,
249
+ ...(flags.stack ? { stack: true } : {}),
250
+ cwd: process.cwd(),
251
+ });
252
+ process.stdout.write(result.report);
253
+ if (result.warnings.length > 0)
254
+ process.stderr.write(`${result.warnings.join("\n")}\n`);
255
+ }
256
+ function tickCommand(argv) {
257
+ const flags = parseFlags(argv);
258
+ if (flags.positionals.length !== 0 || flags.test || hasIntakeFlags(flags) || flags.deliver || flags.base || flags.stack) {
259
+ usage();
260
+ }
261
+ const config = resolveKilnConfig({ configPath: flags.configPath });
262
+ process.stdout.write(runTick(config));
263
+ }
264
+ function statusCommand(argv) {
265
+ const flags = parseFlags(argv);
266
+ if (flags.positionals.length > 1 || flags.test || hasIntakeFlags(flags) || flags.deliver || flags.base || flags.stack) {
267
+ usage();
268
+ }
269
+ const config = resolveKilnConfig({ configPath: flags.configPath });
270
+ process.stdout.write(runStatus(config, flags.positionals[0]));
271
+ }
272
+ function gateCommand(choice, argv) {
273
+ const flags = parseFlags(argv);
274
+ const jobId = flags.positionals[0];
275
+ if (!jobId || flags.positionals.length !== 1 || flags.test || hasIntakeFlags(flags) || flags.deliver || flags.base || flags.stack) {
276
+ usage();
277
+ }
278
+ const config = resolveKilnConfig({ configPath: flags.configPath });
279
+ process.stdout.write(answerJob(config, jobId, choice));
280
+ }
281
+ function hasIntakeFlags(flags) {
282
+ return Boolean(flags.repo ||
283
+ flags.spec ||
284
+ flags.prompt ||
285
+ flags.jira ||
286
+ flags.title ||
287
+ flags.workflow ||
288
+ flags.context ||
289
+ flags.dryRun ||
290
+ flags.stack ||
291
+ flags.after.length > 0);
292
+ }
293
+ async function main() {
294
+ const argv = process.argv.slice(2);
295
+ const command = argv[0];
296
+ if (!command || command === "--help" || command === "-h" || command === "help")
297
+ usage();
298
+ if (command === "repo") {
299
+ repoCommand(argv.slice(1));
300
+ return;
301
+ }
302
+ if (command === "add") {
303
+ await addCommand(argv.slice(1));
304
+ return;
305
+ }
306
+ if (command === "import") {
307
+ importCommand(argv.slice(1));
308
+ return;
309
+ }
310
+ if (command === "tick") {
311
+ tickCommand(argv.slice(1));
312
+ return;
313
+ }
314
+ if (command === "status") {
315
+ statusCommand(argv.slice(1));
316
+ return;
317
+ }
318
+ if (command === "approve") {
319
+ gateCommand("approve", argv.slice(1));
320
+ return;
321
+ }
322
+ if (command === "reject") {
323
+ gateCommand("reject", argv.slice(1));
324
+ return;
325
+ }
326
+ usage();
327
+ }
328
+ try {
329
+ await main();
330
+ }
331
+ catch (error) {
332
+ console.error(error instanceof Error ? error.message : String(error));
333
+ process.exit(1);
334
+ }