@lchase/kiln 0.10.0 → 0.12.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/dist/workflow.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
3
3
  import { basename, join, sep } from "node:path";
4
4
  import { jobBasename, packageRoot } from "./paths.js";
5
5
  const FACTORY_DIRECTIVE = /^<!--\s*factory:\s*(prompts\/factory\/[a-z0-9-]+\.md)\s*-->/;
@@ -12,7 +12,10 @@ const PROMPT_COUNTS = {
12
12
  feature: FEATURE_PROMPTS,
13
13
  investigation: INVESTIGATION_PROMPTS,
14
14
  fix: FIX_PROMPTS,
15
+ review: 1,
15
16
  };
17
+ /** Workflow name for a draft pull-request review. It ends at a gate and does not open a pull request. */
18
+ export const REVIEW_WORKFLOW = "review";
16
19
  /**
17
20
  * Values Kiln can pass into a workflow. A workflow's inputSchema lists the
18
21
  * ones it uses. `description` and `acceptance` stay so
@@ -52,18 +55,7 @@ export function assertWorkflowName(name) {
52
55
  }
53
56
  return path;
54
57
  }
55
- /**
56
- * Yak reads `prompt.file` with `path.resolve(cwd, file)`, and Kiln launches
57
- * yak with cwd set to the product checkout. Rewrite `prompts/kiln/<name>.md`
58
- * to an absolute file under the kiln directory: the Kiln preamble plus the
59
- * verbatim factory prompt. The content hash is part of the filename, so a
60
- * prompt edit changes the path yak caches on.
61
- *
62
- * A preamble that starts with `<!-- factory: prompts/factory/<file>.md -->`
63
- * is combined with that factory body. A prompt without that directive is
64
- * stored as itself. `prompts/kiln/investigation.md` uses the second form.
65
- */
66
- export function materializeWorkflow(kilnDir, jobId, workflowName) {
58
+ export function materializeWorkflow(kilnDir, jobId, workflowName, options = {}) {
67
59
  const sourcePath = assertWorkflowName(workflowName);
68
60
  const source = readFileSync(sourcePath, "utf8");
69
61
  const destDir = join(kilnDir, "rendered", jobBasename(jobId));
@@ -72,7 +64,8 @@ export function materializeWorkflow(kilnDir, jobId, workflowName) {
72
64
  const rendered = source.replace(/file: prompts\/kiln\/([a-z0-9-]+)\.md/g, (_full, name) => {
73
65
  count += 1;
74
66
  const rel = `prompts/kiln/${name}.md`;
75
- const absolute = writeCombinedPrompt(rel, destDir);
67
+ const override = name === "review" ? options.reviewPrompt : undefined;
68
+ const absolute = writeCombinedPrompt(rel, destDir, override);
76
69
  return `file: ${yamlDoubleQuote(absolute)}`;
77
70
  });
78
71
  const expected = PROMPT_COUNTS[workflowName];
@@ -187,8 +180,18 @@ function declaredInputProperties(yaml) {
187
180
  }
188
181
  return names;
189
182
  }
190
- function writeCombinedPrompt(preambleRel, destDir) {
183
+ function writeCombinedPrompt(preambleRel, destDir, overridePath) {
191
184
  const root = packageRoot();
185
+ if (overridePath) {
186
+ if (!existsSync(overridePath) || !statSync(overridePath).isFile()) {
187
+ throw new Error("Review prompt file was not found.");
188
+ }
189
+ const combined = `${readFileSync(overridePath, "utf8").trimEnd()}\n`;
190
+ const digest = createHash("sha256").update(combined).digest("hex").slice(0, 16);
191
+ const outPath = join(destDir, `${basename(preambleRel, ".md")}-${digest}.md`);
192
+ writeFileSync(outPath, combined);
193
+ return outPath;
194
+ }
192
195
  const preamblePath = join(root, preambleRel);
193
196
  const preamble = readFileSync(preamblePath, "utf8");
194
197
  const directive = preamble.match(FACTORY_DIRECTIVE);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lchase/kiln",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "Run a workflow on any registered repo. A markdown spec is the input.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -16,7 +16,8 @@
16
16
  ],
17
17
  "repository": {
18
18
  "type": "git",
19
- "url": "git+https://github.com/lchase/kiln.git"
19
+ "url": "git+https://github.com/lchase/kiln.git",
20
+ "directory": "packages/kiln"
20
21
  },
21
22
  "publishConfig": {
22
23
  "access": "public"
@@ -0,0 +1,13 @@
1
+ Kiln runs this review inside yak. The working directory is an isolated worktree at `{{input.baseRef}}`. That commit is the pull request head. Do not guess a branch name.
2
+
3
+ Read the change. Return a draft review and nothing else. Do not post the review, approve the pull request, merge it, push, or comment. Do not change product code. Do not create, modify, or delete files. Read-only git and file inspection are allowed.
4
+
5
+ Return JSON with `markdown` and `findings`. `findings` is a list. Each item has `severity` (`note`, `nit`, `warning`, or `blocking`), `file`, `line`, and `comment`. `comment` is the text a person might post later. `markdown` is the whole draft a person will read, and it must include every finding with its severity, file, line, and suggested comment. If you find nothing, return an empty `findings` list and say so in `markdown`.
6
+
7
+ Do not invent files or line numbers. If you did not see a line, do not cite it.
8
+
9
+ Spec title: {{input.title}}
10
+
11
+ {{input.spec}}
12
+
13
+ Launch id: {{input.runNonce}}
@@ -0,0 +1,64 @@
1
+ # Draft review of one pull request, run by yak (@lchase/yak).
2
+ #
3
+ # The agent returns findings (severity, file, line, suggested comment).
4
+ # A person approves or rejects the draft in the Inbox. Approving does not
5
+ # post, approve, or merge on GitHub: Kiln has no gated posting path for a
6
+ # review comment, so the draft stops here.
7
+ #
8
+ # `reviewPrompt` in the kiln config replaces prompts/kiln/review.md.
9
+ # The packaged file is the generic default.
10
+ #
11
+ # `adapter: "mock"` reads `test/fixtures/kiln-review/draft.json` from the checkout.
12
+ name: kiln-review
13
+ version: "1"
14
+ inputSchema:
15
+ inline:
16
+ type: object
17
+ additionalProperties: false
18
+ properties:
19
+ title: { type: string }
20
+ spec: { type: string }
21
+ runNonce: { type: string }
22
+ baseRef: { type: string }
23
+ required: [title, spec, runNonce, baseRef]
24
+ steps:
25
+ - id: draft
26
+ needs: [input]
27
+ agent:
28
+ model: opus
29
+ prompt:
30
+ file: prompts/kiln/review.md
31
+ schema:
32
+ inline:
33
+ type: object
34
+ additionalProperties: false
35
+ properties:
36
+ markdown: { type: string, minLength: 1 }
37
+ findings:
38
+ type: array
39
+ items:
40
+ type: object
41
+ additionalProperties: false
42
+ properties:
43
+ severity: { type: string, enum: [note, nit, warning, blocking] }
44
+ file: { type: string, minLength: 1 }
45
+ line: { type: integer, minimum: 1 }
46
+ comment: { type: string, minLength: 1 }
47
+ required: [severity, file, line, comment]
48
+ required: [markdown, findings]
49
+ tools: [Read, Grep, Glob, Bash]
50
+ produces: draft
51
+
52
+ - id: approve
53
+ needs: [input, draft]
54
+ gate:
55
+ schema:
56
+ inline:
57
+ type: object
58
+ additionalProperties: false
59
+ properties:
60
+ decision: { type: string, enum: [approve, reject] }
61
+ required: [decision]
62
+ render:
63
+ inline: "Draft review for \"{{input.title}}\"?\n\nApproving keeps this draft. Kiln does not post it to GitHub.\n\n{{draft.markdown}}"
64
+ produces: approval