@lchase/kiln 0.3.0 → 0.5.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
@@ -6,10 +6,12 @@ const FACTORY_DIRECTIVE = /^<!--\s*factory:\s*(prompts\/factory\/[a-z0-9-]+\.md)
6
6
  const WORKFLOW_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
7
7
  const FEATURE_PROMPTS = 7;
8
8
  const INVESTIGATION_PROMPTS = 1;
9
+ const FIX_PROMPTS = 1;
9
10
  /** Prompt files Kiln rewrites before launch. A mismatch refuses the launch. */
10
11
  const PROMPT_COUNTS = {
11
12
  feature: FEATURE_PROMPTS,
12
13
  investigation: INVESTIGATION_PROMPTS,
14
+ fix: FIX_PROMPTS,
13
15
  };
14
16
  /**
15
17
  * Values Kiln can pass into a workflow. A workflow's inputSchema lists the
@@ -81,6 +83,73 @@ export function workflowInputs(workflowYaml, input) {
81
83
  }
82
84
  return out;
83
85
  }
86
+ /**
87
+ * Leaf step ids under the root `steps:` block, in file order.
88
+ * A step that contains nested `- id:` entries is a loop or map container
89
+ * and is omitted; its nested ids are the ones a run actually starts.
90
+ */
91
+ export function workflowLeafSteps(yaml) {
92
+ const lines = yaml.split("\n");
93
+ let inSteps = false;
94
+ const nodes = [];
95
+ for (const line of lines) {
96
+ if (!inSteps) {
97
+ if (line === "steps:" || line.startsWith("steps:"))
98
+ inSteps = true;
99
+ continue;
100
+ }
101
+ if (/^\S/.test(line))
102
+ break;
103
+ const match = /^(\s*)- id:\s*([A-Za-z0-9._-]+)\s*(?:#.*)?$/.exec(line);
104
+ if (!match?.[2])
105
+ continue;
106
+ nodes.push({ id: match[2], indent: match[1]?.length ?? 0 });
107
+ }
108
+ const leaves = [];
109
+ for (let index = 0; index < nodes.length; index += 1) {
110
+ const node = nodes[index];
111
+ const next = nodes[index + 1];
112
+ if (!node)
113
+ continue;
114
+ if (next && next.indent > node.indent)
115
+ continue;
116
+ leaves.push(node.id);
117
+ }
118
+ return leaves;
119
+ }
120
+ /** Leaf step ids from a yak `workflow.json` `steps` array. Loop and map containers expand to their body. */
121
+ export function leafStepIds(steps) {
122
+ if (!Array.isArray(steps))
123
+ return [];
124
+ const ids = [];
125
+ for (const step of steps) {
126
+ if (typeof step !== "object" || step === null || !("id" in step))
127
+ continue;
128
+ const id = step.id;
129
+ if (typeof id !== "string" || id.length === 0)
130
+ continue;
131
+ const kind = "kind" in step ? step.kind : undefined;
132
+ if (kind === "loop" && "body" in step) {
133
+ ids.push(...leafStepIds(step.body));
134
+ continue;
135
+ }
136
+ if (kind === "map" && "step" in step) {
137
+ ids.push(...leafStepIds([step.step]));
138
+ continue;
139
+ }
140
+ ids.push(id);
141
+ }
142
+ return ids;
143
+ }
144
+ /** Leaf steps of a packaged workflow. An unknown name is an empty plan, not an error. */
145
+ export function workflowStepPlan(name) {
146
+ if (!WORKFLOW_NAME.test(name))
147
+ return [];
148
+ const path = join(packageRoot(), "workflows", `${name}.yaml`);
149
+ if (!existsSync(path))
150
+ return [];
151
+ return workflowLeafSteps(readFileSync(path, "utf8"));
152
+ }
84
153
  /** True when the workflow runs the repo test command. Investigation does not. */
85
154
  export function workflowNeedsTestCommand(workflowYaml) {
86
155
  return /\bKILN_TEST\b/.test(workflowYaml) || workflowYaml.includes("KILN_VERIFY_WRAPPER");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lchase/kiln",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Run a workflow on any registered repo. A markdown spec is the input.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,17 @@
1
+ <!-- factory: prompts/factory/builder.md -->
2
+
3
+ Kiln runs this factory agent inside yak to answer pull request feedback. The working directory is the worktree (there is no separate worktree path). There is no `.factory/` job folder and no `.factory/repo.md`; read the repo for conventions. `.factory/lessons.md` applies only when that file is really present.
4
+
5
+ `<main branch>` is the base SHA `{{input.baseRef}}` (the pull request head this worktree was branched from, via yak `--from`). Use that SHA wherever the factory instructions say `<main branch>`. Do not guess `main`.
6
+
7
+ Your branch is whatever `git branch --show-current` prints. It is `yak/<run-id>`. That is the branch name in this prompt. Do not BLOCK because it is not `factory/...` or `main`.
8
+
9
+ Commit on that branch. Do not push. Do not force-push. Do not open a pull request. Do not approve one. Do not comment on one. Kiln fast-forwards the existing pull request branch after this run.
10
+
11
+ Do not write `build.md`. Return JSON: `status` is `READY` or `BLOCKED`, `summary` is what you changed. The original spec and the feedback items are the task text below, not files.
12
+
13
+ Task: {{input.title}}
14
+
15
+ {{input.description}}
16
+
17
+ The factory agent instructions follow. Their frontmatter is the original definition; follow the prose, and return the JSON above instead of writing the file they name.
@@ -0,0 +1,44 @@
1
+ # One pull-request fix, run by yak (@lchase/yak).
2
+ #
3
+ # The same builder as workflows/feature.yaml (factory builder, opus). Kiln
4
+ # launches it with `yak run --isolation worktree --from <pull request head>`
5
+ # on a job whose pull request is already open. Yak cuts a worktree branch
6
+ # `yak/<run-id>` from that commit. The agent commits there. Kiln fast-forwards
7
+ # the open pull request branch. This workflow does not open, approve, merge,
8
+ # or comment on a pull request, and it does not force-push.
9
+ name: kiln-fix
10
+ version: "1"
11
+ inputSchema:
12
+ inline:
13
+ type: object
14
+ additionalProperties: false
15
+ properties:
16
+ title: { type: string }
17
+ description: { type: string }
18
+ acceptance: { type: string }
19
+ runNonce: { type: string }
20
+ baseRef: { type: string }
21
+ required: [title, description, acceptance, runNonce, baseRef]
22
+ steps:
23
+ - id: build
24
+ needs: [input]
25
+ agent:
26
+ model: opus
27
+ prompt:
28
+ file: prompts/kiln/fix.md
29
+ schema:
30
+ inline:
31
+ type: object
32
+ additionalProperties: false
33
+ properties:
34
+ summary: { type: string }
35
+ status: { type: string, enum: [READY, BLOCKED] }
36
+ required: [summary, status]
37
+ tools: [Read, Grep, Glob, Bash, Write, Edit, Skill]
38
+ produces: build
39
+
40
+ - id: land
41
+ needs: [input, build]
42
+ skipIf: 'build.status != "READY"'
43
+ command:
44
+ run: 'git add -A && git diff --cached --quiet || git commit -m "kiln: $KILN_TASK_TITLE"'