@netnodeag/kraftwerk 0.2.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 +339 -0
- package/bin/kraftwerk.js +24 -0
- package/dist/agent.d.ts +49 -0
- package/dist/agent.js +3 -0
- package/dist/cli/create-brief.d.ts +4 -0
- package/dist/cli/create-brief.js +146 -0
- package/dist/cli/doctor.d.ts +1 -0
- package/dist/cli/doctor.js +87 -0
- package/dist/cli/init.d.ts +1 -0
- package/dist/cli/init.js +81 -0
- package/dist/cli/kraftwerk.d.ts +1 -0
- package/dist/cli/kraftwerk.js +342 -0
- package/dist/cli/runs.d.ts +6 -0
- package/dist/cli/runs.js +120 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +47 -0
- package/dist/config.d.ts +41 -0
- package/dist/config.js +97 -0
- package/dist/discover.d.ts +21 -0
- package/dist/discover.js +38 -0
- package/dist/envelope.d.ts +21 -0
- package/dist/envelope.js +61 -0
- package/dist/gates.d.ts +18 -0
- package/dist/gates.js +34 -0
- package/dist/harness.d.ts +66 -0
- package/dist/harness.js +14 -0
- package/dist/harnesses/claude.d.ts +2 -0
- package/dist/harnesses/claude.js +117 -0
- package/dist/harnesses/codex.d.ts +2 -0
- package/dist/harnesses/codex.js +158 -0
- package/dist/harnesses/pi.d.ts +2 -0
- package/dist/harnesses/pi.js +151 -0
- package/dist/harnesses/registry.d.ts +2 -0
- package/dist/harnesses/registry.js +19 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +22 -0
- package/dist/remote.d.ts +23 -0
- package/dist/remote.js +57 -0
- package/dist/run.d.ts +66 -0
- package/dist/run.js +278 -0
- package/dist/runner/docker.d.ts +38 -0
- package/dist/runner/docker.js +166 -0
- package/dist/stats.d.ts +46 -0
- package/dist/stats.js +65 -0
- package/dist/validate.d.ts +8 -0
- package/dist/validate.js +33 -0
- package/dist/workflow.d.ts +24 -0
- package/dist/workflow.js +11 -0
- package/dist/yaml.d.ts +21 -0
- package/dist/yaml.js +324 -0
- package/package.json +52 -0
- package/runner/Dockerfile +43 -0
- package/schema/workflow.schema.json +244 -0
package/README.md
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
# kraftwerk
|
|
2
|
+
|
|
3
|
+
Deterministic workflow-as-code over headless agent harnesses, in the spirit of
|
|
4
|
+
[super-simple-software-factory](https://github.com/disler/super-simple-software-factory):
|
|
5
|
+
**code owns the control flow, agents work inside bounded phases.**
|
|
6
|
+
"Agent proposes, code disposes."
|
|
7
|
+
|
|
8
|
+
No SDK dependency: every agent phase spawns one short-lived CLI process on the
|
|
9
|
+
agent's **harness** and judges the result afterwards (typed JSON envelope +
|
|
10
|
+
file gates). Failed checks are corrected in the same session, never by a cold
|
|
11
|
+
restart. Every run leaves a `trace.jsonl` event log and ends with a
|
|
12
|
+
time/token/cost summary table.
|
|
13
|
+
|
|
14
|
+
## Consume
|
|
15
|
+
|
|
16
|
+
Zero-setup consumer (YAML workflows only): a repo containing workflow
|
|
17
|
+
folders under `workflows/` (or `src/workflows/`) IS a complete consumer —
|
|
18
|
+
no package.json, no install:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx @netnodeag/kraftwerk init # scaffold kraftwerk.yml + workflows/ + example
|
|
22
|
+
npx @netnodeag/kraftwerk run hello "Was ist kraftwerk?"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
For a local checkout / programmatic consumer (TS workflows, custom gates,
|
|
26
|
+
approval loops), add the dependency (the `kraftwerk` alias keeps imports
|
|
27
|
+
short):
|
|
28
|
+
|
|
29
|
+
```jsonc
|
|
30
|
+
// package.json of your workflow project
|
|
31
|
+
"dependencies": { "kraftwerk": "file:../kraftwerk" } // or: "npm:@netnodeag/kraftwerk"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { defineAgent, Run, runCli, fileNonEmpty, envelopeContract } from "kraftwerk";
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## CLI — kraftwerk
|
|
39
|
+
|
|
40
|
+
Ships with the package (`npx @netnodeag/kraftwerk …` anywhere, `npm link` in
|
|
41
|
+
the checkout for a global `kraftwerk`). Workflows are auto-discovered under
|
|
42
|
+
`src/workflows/` (or `workflows/`): every folder with a `workflow.yml` and
|
|
43
|
+
every top-level `.yml` file. Every command works from any subdirectory —
|
|
44
|
+
the CLI walks up to the project root (marked by `kraftwerk.yml`, a
|
|
45
|
+
workflows root, or `.git`).
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
kraftwerk init # make this repo a consumer: kraftwerk.yml, workflows/, example
|
|
49
|
+
kraftwerk list # table: workflows, steps, agents (with harness/model); --json
|
|
50
|
+
kraftwerk run tagline "https://..." # run; --yes, --verbose
|
|
51
|
+
kraftwerk run # interactive: pick workflow, type the request
|
|
52
|
+
kraftwerk runs # past runs from output/*/trace.jsonl; runs show <id> for detail
|
|
53
|
+
kraftwerk doctor # preflight: harness CLIs, docker, workflows, declared env vars
|
|
54
|
+
kraftwerk validate # all discovered — schema + semantics + files, exit 1 on failure
|
|
55
|
+
kraftwerk validate src/workflows/pitch # specific paths
|
|
56
|
+
kraftwerk create "was der Workflow tun soll" # for LLM agents: prints a build brief
|
|
57
|
+
kraftwerk runner build # build the Docker sandbox image (once)
|
|
58
|
+
kraftwerk run --sandbox website-check "https://..." # isolated container per run; --ssh forwards the agent
|
|
59
|
+
kraftwerk runner ps / stop <run-id> # see / stop running sandbox containers
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Project config — kraftwerk.yml
|
|
63
|
+
|
|
64
|
+
Optional, at the project root (also the root marker for the walk-up); all
|
|
65
|
+
fields optional: `workflows:` (workflows root) and `output:` (run-artifact
|
|
66
|
+
directory, default `output/`).
|
|
67
|
+
|
|
68
|
+
### Triggering from CI / cron / webhooks
|
|
69
|
+
|
|
70
|
+
`run --json` is the machine mode: non-interactive, one JSON result object
|
|
71
|
+
on stdout (`ok`, `runDir`, per-phase stats, totals), all narration on
|
|
72
|
+
stderr. `KRAFTWERK_YES=1` equals `--yes`, `--quiet` silences narration.
|
|
73
|
+
Exit codes: 0 ok, 2 usage/config error (unknown workflow, missing env),
|
|
74
|
+
3 run failed (gate/blocked/harness), 1 unexpected.
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
KRAFTWERK_YES=1 npx @netnodeag/kraftwerk run tagline "https://..." --json > result.json
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Workflows declare the env vars they need via top-level `requires:
|
|
81
|
+
[MATOMO_TOKEN, ...]` — checked before anything spawns, listed by
|
|
82
|
+
`kraftwerk list` and `kraftwerk doctor`.
|
|
83
|
+
|
|
84
|
+
### Remote workflows — --from
|
|
85
|
+
|
|
86
|
+
`list` and `run` accept `--from github:org/repo[@ref]` (or any git URL):
|
|
87
|
+
the repo is shallow-cloned to `~/.cache/kraftwerk/remotes/` (refreshed per
|
|
88
|
+
call, cached offline) and its workflows run locally — artifacts land in
|
|
89
|
+
YOUR `output/`, not the cache. Share one workflow library across projects
|
|
90
|
+
without vendoring:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npx @netnodeag/kraftwerk run --from github:NETNODEAG/workflows tagline "https://..."
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Sandbox mode (`--sandbox`) runs the workflow in a `kraftwerk-runner`
|
|
97
|
+
container (see `runner/Dockerfile`): workflow folder mounted read-only,
|
|
98
|
+
the run directory bind-mounted straight into the host `output/` — trace
|
|
99
|
+
and artifacts appear live, no copy-back. Env vars come from
|
|
100
|
+
`<project>/runner.env` (plus `ANTHROPIC_API_KEY`/`OPENAI_API_KEY`
|
|
101
|
+
pass-through); `--run-id` pins the run folder name for external triggers
|
|
102
|
+
(the inspector uses this). `runner.json` in the run dir records
|
|
103
|
+
container, exit code, and timing.
|
|
104
|
+
|
|
105
|
+
`run` prompts for whatever is missing (workflow picker, request input);
|
|
106
|
+
invalid workflows show up red in `list` with their validation error
|
|
107
|
+
instead of breaking the listing. `create` is meant to be run BY an LLM
|
|
108
|
+
agent (Claude Code, Codex): it prints a self-contained brief — schema
|
|
109
|
+
example, gates, harness rules, verify ladder — that the agent follows to
|
|
110
|
+
author the workflow folder and validate/smoke it with this CLI.
|
|
111
|
+
|
|
112
|
+
## The agent — four axes
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
export const desloper = defineAgent({
|
|
116
|
+
id: "desloper",
|
|
117
|
+
name: "Lektorat",
|
|
118
|
+
harness: "codex", // WHERE it runs: claude (default) | codex | pi
|
|
119
|
+
model: "gpt-5.6-sol", // WHAT thinks, in the harness's naming
|
|
120
|
+
effort: "high", // optional: low | medium | high | xhigh | max
|
|
121
|
+
tools: ["Read", "Write", "Edit"], // governance: capability boundary
|
|
122
|
+
persona: `Du bist Lektor:in ...`, // WHO: the system prompt
|
|
123
|
+
clis: { // optional: CLI grants — the hint is injected
|
|
124
|
+
git: "Versionierung; nach jedem Schritt committen", // into the persona ONCE
|
|
125
|
+
},
|
|
126
|
+
mcp: { // optional: MCP servers (governance, like tools)
|
|
127
|
+
calculator: { command: "node", args: ["/path/to/multiply-server.ts"] },
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
The task arrives per phase, so one agent can serve several phases. Phases on
|
|
133
|
+
the same harness share one resumed session (an agent sees the conversation so
|
|
134
|
+
far but always speaks with its own persona); phases on different harnesses
|
|
135
|
+
share state through the run files only.
|
|
136
|
+
|
|
137
|
+
## Primitives
|
|
138
|
+
|
|
139
|
+
| Primitive | What it does |
|
|
140
|
+
| --------- | ------------ |
|
|
141
|
+
| `defineAgent` ([src/agent.ts](src/agent.ts)) | persona + model/effort + tools + harness |
|
|
142
|
+
| `Run.agentPhase({name, agent, prompt, gates})` ([src/run.ts](src/run.ts)) | spawn → parse envelope → run gates → correct in-session (bounded by `maxGateRetries`) |
|
|
143
|
+
| `Run.codePhase(name, fn)` | deterministic step, timed and traced |
|
|
144
|
+
| Gates ([src/gates.ts](src/gates.ts)) | post-execution file checks: `fileNonEmpty`, `slotsFilled`, `containsText` — or your own `Gate` |
|
|
145
|
+
| Envelope ([src/envelope.ts](src/envelope.ts)) | every phase prompt ends with `envelopeContract(phase)`; `parseEnvelope` enforces it |
|
|
146
|
+
| Stats ([src/stats.ts](src/stats.ts)) | per-phase attempts/time/tokens/cost, `run.printSummary()` renders the table |
|
|
147
|
+
| `runCli(workflows)` ([src/cli.ts](src/cli.ts)) | registry CLI: `npm start -- <name> [--yes] [--verbose] "<request>"` |
|
|
148
|
+
| `trace.jsonl` | every event: phase start/end, tool calls, envelopes, gate results, stats |
|
|
149
|
+
|
|
150
|
+
## Harnesses
|
|
151
|
+
|
|
152
|
+
One adapter per runtime ([src/harnesses/](src/harnesses)), all speaking the
|
|
153
|
+
same interface ([src/harness.ts](src/harness.ts)):
|
|
154
|
+
|
|
155
|
+
| | claude (default) | codex | pi |
|
|
156
|
+
| --- | --- | --- | --- |
|
|
157
|
+
| Process | `claude -p --output-format stream-json` | `codex exec --json` | `pi -p --mode json` |
|
|
158
|
+
| Resume | `--resume <id>` | `exec resume <thread-id>` | `--session-id <id>` (create-or-continue) |
|
|
159
|
+
| Auth | Claude Code login | ChatGPT login | Claude/ChatGPT OAuth **or** vendor API keys |
|
|
160
|
+
| Models | Claude ids | GPT ids | `provider/id`, e.g. `deepseek/deepseek-chat`, `openrouter/...` |
|
|
161
|
+
| Hermetic | `--setting-sources ""` | `--ignore-user-config` | `--no-context-files` |
|
|
162
|
+
| MCP | `--mcp-config` + `--strict-mcp-config`, allowlist `mcp__<name>` | `-c mcp_servers.*` + `--approve-for-me` (headless approvals) | not supported (own extension system) |
|
|
163
|
+
| CLIs | scoped allowlist `Bash(<name>:*)` | sandbox runs them anyway (hint only) | plain `bash` tool (no scoping) |
|
|
164
|
+
| Quirks | — | no system-prompt flag (persona prepended to prompt); governance = workspace-write sandbox, not per-tool | `effort` maps 1:1 to `--thinking`; tool names lowercased |
|
|
165
|
+
|
|
166
|
+
Prerequisites: **claude** — Claude Code installed + logged in. **codex** —
|
|
167
|
+
`brew install --cask codex` + `codex login`. **pi** — `npm install -g
|
|
168
|
+
@earendil-works/pi-coding-agent`; Anthropic models reuse the Claude
|
|
169
|
+
subscription OAuth, other vendors need their key in the env (check with
|
|
170
|
+
`pi auth check --provider deepseek`).
|
|
171
|
+
|
|
172
|
+
## YAML workflows
|
|
173
|
+
|
|
174
|
+
Linear workflows can be pure config — GitHub-Actions-flavored (`steps`,
|
|
175
|
+
`runs-on`, `${{ request }}` / `${{ agent }}`). The canonical form is a
|
|
176
|
+
**folder**: `workflow.yml` holds agents + steps, long prompts live as files
|
|
177
|
+
next to it. Loaded with `loadWorkflow(path)` and registered like any other
|
|
178
|
+
workflow:
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
src/workflows/tagline/
|
|
182
|
+
workflow.yml # agents inline + steps
|
|
183
|
+
prompts/
|
|
184
|
+
analysieren.md # referenced from a step, may use ${{ request }}
|
|
185
|
+
texten.md
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
```yaml
|
|
189
|
+
# yaml-language-server: $schema=https://raw.githubusercontent.com/NETNODEAG/kraftwerk/main/kraftwerk/schema/workflow.schema.json
|
|
190
|
+
name: tagline
|
|
191
|
+
description: "Tagline Generator (YAML)"
|
|
192
|
+
workspace: |
|
|
193
|
+
Dateien: brand.md, tagline.md
|
|
194
|
+
agents:
|
|
195
|
+
analyst:
|
|
196
|
+
runs-on: claude # claude (default) | codex | pi
|
|
197
|
+
model: haiku # effort: low..max optional
|
|
198
|
+
tools: [Read, Write, Edit, WebFetch]
|
|
199
|
+
persona: |
|
|
200
|
+
Du analysierst Marken ...
|
|
201
|
+
steps:
|
|
202
|
+
- name: analysieren
|
|
203
|
+
agent: analyst
|
|
204
|
+
prompt: prompts/analysieren.md # single-line value = file in the folder
|
|
205
|
+
gates:
|
|
206
|
+
- file_non_empty: brand.md
|
|
207
|
+
- contains: { file: brand.md, text: "## Tonalitaet", label: Tonalitaet }
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Running it needs no code at all — `kraftwerk run tagline "..."` discovers
|
|
211
|
+
the folder. Programmatic registration works too:
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
const tagline = await loadWorkflow(path.join(import.meta.dirname, "workflows/tagline"));
|
|
215
|
+
runCli({ [tagline.name]: tagline });
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Single-line `prompt:`/`persona:`/`workspace:` values are file references
|
|
219
|
+
inside the folder; multiline values stay inline (a plain single `.yml` file
|
|
220
|
+
with everything inline works too). `${{ agent }}` interpolates the step's
|
|
221
|
+
agent id — three jury steps can share one `prompts/assess.md` that writes
|
|
222
|
+
`verdict-${{ agent }}.md` (see the pitch workflow). The engine appends the
|
|
223
|
+
envelope contract to every step prompt itself.
|
|
224
|
+
|
|
225
|
+
**Validation**: [`schema/workflow.schema.json`](schema/workflow.schema.json)
|
|
226
|
+
(strict — unknown keys are errors, editors autocomplete via the
|
|
227
|
+
`# yaml-language-server: $schema=…` line) plus semantic checks (agent
|
|
228
|
+
references, duplicate steps, variables, referenced files):
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
kraftwerk validate # all discovered workflows
|
|
232
|
+
kraftwerk validate src/workflows/tagline # specific paths
|
|
233
|
+
npm start -- validate <path> # runCli consumers (TS registry)
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Gates: `file_non_empty: <file>`, `slots_filled: <file>`,
|
|
237
|
+
`contains: {file, text, label?}`. Living examples:
|
|
238
|
+
[`../agent-playground/src/workflows/tagline/`](../agent-playground/src/workflows/tagline/)
|
|
239
|
+
and [`../agent-playground/src/workflows/pitch/`](../agent-playground/src/workflows/pitch/).
|
|
240
|
+
v1 is deliberately linear — approval loops, AGENTS.md-style context files
|
|
241
|
+
and skills stay on the roadmap; anything non-linear is a TS workflow.
|
|
242
|
+
|
|
243
|
+
### MCP servers alongside the workflow
|
|
244
|
+
|
|
245
|
+
A workflow folder can carry its own MCP servers; agents opt in by name
|
|
246
|
+
(governance, like `tools`). Relative files resolve inside the folder,
|
|
247
|
+
absolute paths and `url:` entries hook up external/remote servers:
|
|
248
|
+
|
|
249
|
+
```yaml
|
|
250
|
+
mcp:
|
|
251
|
+
calculator:
|
|
252
|
+
command: node # node >= 24 runs TypeScript directly
|
|
253
|
+
args: [mcp/multiply-server.ts] # file inside the workflow folder
|
|
254
|
+
linear:
|
|
255
|
+
url: https://mcp.linear.app/mcp # remote streamable HTTP
|
|
256
|
+
agents:
|
|
257
|
+
rechner:
|
|
258
|
+
model: sonnet
|
|
259
|
+
tools: [Read, Write]
|
|
260
|
+
mcp: [calculator] # this agent may use these servers
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
The stdio server is any MCP server (e.g. `@modelcontextprotocol/sdk` +
|
|
264
|
+
`server.tool(...)` + `StdioServerTransport`, its deps in the consumer's
|
|
265
|
+
`package.json`). On claude the servers are passed hermetically
|
|
266
|
+
(`--strict-mcp-config`) and the allowlist gains `mcp__<name>`; on codex
|
|
267
|
+
they become `-c mcp_servers.*` overrides and the phase runs with
|
|
268
|
+
`--approve-for-me` so headless MCP calls get approved; `runs-on: pi` +
|
|
269
|
+
`mcp` is rejected at validation time. Living example:
|
|
270
|
+
[`../agent-playground/src/workflows/rechner/`](../agent-playground/src/workflows/rechner/).
|
|
271
|
+
|
|
272
|
+
### CLI grants
|
|
273
|
+
|
|
274
|
+
For existing command-line tools an MCP server is overkill — declare them
|
|
275
|
+
once and grant per agent, so no step prompt has to repeat which CLIs
|
|
276
|
+
exist or how to call them:
|
|
277
|
+
|
|
278
|
+
```yaml
|
|
279
|
+
clis: # command prefix -> one-line usage hint
|
|
280
|
+
my: "CLI fuer my.netnode.ch. Immer --json und -w <workspace-id> verwenden."
|
|
281
|
+
git: "" # empty hint = name only
|
|
282
|
+
agents:
|
|
283
|
+
reporter:
|
|
284
|
+
tools: [Read, Write]
|
|
285
|
+
clis: [my, git] # this agent may call these via Bash
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
The hint is injected into the agent's persona ONCE (that's the point —
|
|
289
|
+
step prompts stay clean). Per harness: **claude** additionally scopes the
|
|
290
|
+
Bash allowlist to `Bash(<name>:*)` — the granted prefixes run headless
|
|
291
|
+
without approval, everything else keeps claude's default judgment
|
|
292
|
+
(read-only commands auto-approve, mutating ones are denied). **codex**
|
|
293
|
+
needs nothing (the workspace-write sandbox runs commands anyway).
|
|
294
|
+
**pi** has no per-command scoping — a grant enables the plain `bash`
|
|
295
|
+
tool.
|
|
296
|
+
|
|
297
|
+
## Used by
|
|
298
|
+
|
|
299
|
+
- [`../agent-playground/`](../agent-playground/) — the in-repo consumer with
|
|
300
|
+
the YAML example workflows (`tagline`, `pitch`, `rechner` with its own
|
|
301
|
+
MCP server, `website-check` with script steps).
|
|
302
|
+
- `nn-content-workflow-2` (in the local `langgraph/` experiments folder,
|
|
303
|
+
outside this repo) — the netnode.ch content board and Matomo report
|
|
304
|
+
generator; the full ADW pattern including the engineer approval gate and
|
|
305
|
+
revision loop.
|
|
306
|
+
|
|
307
|
+
To scaffold a new workflow, use the repo-root skill `/new-workflow`.
|
|
308
|
+
|
|
309
|
+
## Developer
|
|
310
|
+
|
|
311
|
+
Source is TypeScript under `src/`; the published package ships compiled
|
|
312
|
+
JavaScript + type declarations under `dist/` (built by `tsc -p
|
|
313
|
+
tsconfig.build.json`). The bin shim `bin/kraftwerk.js` runs the TS source
|
|
314
|
+
via `tsx` whenever `src/` is present (dev checkout, `npm link`) — edits
|
|
315
|
+
are always live, a stale `dist/` can never shadow them. Published
|
|
316
|
+
installs contain no `src/`, so they take the compiled `dist/` path.
|
|
317
|
+
`KRAFTWERK_DIST=1 kraftwerk …` forces `dist/` from the checkout, e.g. to
|
|
318
|
+
verify a fresh build.
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
npm run typecheck # tsc --noEmit over src/
|
|
322
|
+
npm run validate # validate the example workflows
|
|
323
|
+
npm run build # clean + compile src/ -> dist/ (JS + .d.ts)
|
|
324
|
+
npm link # global `kraftwerk` command from this checkout (no build needed)
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
### Publishing
|
|
328
|
+
|
|
329
|
+
`prepublishOnly` runs the build automatically, so publishing is just:
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
npm publish # runs npm run build first via prepublishOnly
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
The tarball is whitelisted via `files`: `bin/`, `dist/`, `runner/`
|
|
336
|
+
(Dockerfile for sandboxed runs), `schema/` (workflow JSON schema) — no
|
|
337
|
+
`src/`, examples, or inspector. Check with `npm pack --dry-run` before a
|
|
338
|
+
release. Runtime deps stay regular `dependencies`; `tsx` and `typescript`
|
|
339
|
+
are dev-only, so consumers install neither.
|
package/bin/kraftwerk.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// kraftwerk bin shim. Dev checkouts (src/ present, e.g. via npm link) run
|
|
3
|
+
// the TypeScript source through tsx so edits are always live — a stale
|
|
4
|
+
// dist/ can never shadow them. Published installs ship no src/, so they
|
|
5
|
+
// take the compiled dist/ path. KRAFTWERK_DIST=1 forces dist/ (e.g. to
|
|
6
|
+
// verify a build from the checkout).
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
11
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const src = path.join(here, "../src/cli/kraftwerk.ts");
|
|
13
|
+
const dist = path.join(here, "../dist/cli/kraftwerk.js");
|
|
14
|
+
|
|
15
|
+
if (existsSync(src) && process.env.KRAFTWERK_DIST !== "1") {
|
|
16
|
+
const { register } = await import("tsx/esm/api");
|
|
17
|
+
register();
|
|
18
|
+
await import(src);
|
|
19
|
+
} else if (existsSync(dist)) {
|
|
20
|
+
await import(dist);
|
|
21
|
+
} else {
|
|
22
|
+
console.error("kraftwerk: neither src/ nor dist/ found — broken install?");
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
package/dist/agent.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { HarnessId, McpServerConfig } from "./harness.js";
|
|
2
|
+
/**
|
|
3
|
+
* An agent is exactly four things:
|
|
4
|
+
*
|
|
5
|
+
* persona — WHO it is: the system prompt (role, voice, editorial rules)
|
|
6
|
+
* model — WHAT thinks: the model id + optional effort level
|
|
7
|
+
* governance — WHAT it may do: the allowed tools (capability boundary)
|
|
8
|
+
* harness — WHERE it runs: the agent runtime (claude -p, codex exec, pi)
|
|
9
|
+
*
|
|
10
|
+
* Nothing else. The task an agent works on comes from the phase prompt at
|
|
11
|
+
* call time (`run.agentPhase({ agent, prompt, gates })`), so one agent can
|
|
12
|
+
* serve several phases. How an agent is executed (headless process, session
|
|
13
|
+
* resume, envelope, gates) is entirely the framework's business — see
|
|
14
|
+
* harness.ts, harnesses/, and run.ts.
|
|
15
|
+
*/
|
|
16
|
+
export type EffortLevel = "low" | "medium" | "high" | "xhigh" | "max";
|
|
17
|
+
export interface AgentDefinition {
|
|
18
|
+
/** Stable id, used in logs and trace.jsonl. */
|
|
19
|
+
id: string;
|
|
20
|
+
/** Human-readable role name, shown when the phase starts. */
|
|
21
|
+
name: string;
|
|
22
|
+
/** Model id in the harness's naming, e.g. "claude-opus-5" or "gpt-5.6-sol". */
|
|
23
|
+
model: string;
|
|
24
|
+
/** Reasoning effort for this agent. Omit for the model's default. */
|
|
25
|
+
effort?: EffortLevel;
|
|
26
|
+
/** System prompt: role, voice, and the rules this agent always applies. */
|
|
27
|
+
persona: string;
|
|
28
|
+
/** Governance: the only tools this agent is allowed to use. */
|
|
29
|
+
tools: string[];
|
|
30
|
+
/**
|
|
31
|
+
* CLIs this agent may call via Bash: command prefix -> one-line usage
|
|
32
|
+
* hint. The hints are injected into the persona ONCE, so step prompts
|
|
33
|
+
* never repeat them. On claude each name additionally becomes a scoped
|
|
34
|
+
* `Bash(<name>:*)` allowlist entry; codex runs commands in its sandbox
|
|
35
|
+
* anyway (hint only); pi has no per-command scoping and gets the plain
|
|
36
|
+
* bash tool instead.
|
|
37
|
+
*/
|
|
38
|
+
clis?: Record<string, string>;
|
|
39
|
+
/**
|
|
40
|
+
* MCP servers this agent may use, keyed by server name (part of the
|
|
41
|
+
* governance boundary, like tools). Stdio servers can live right next to
|
|
42
|
+
* the workflow; `url` entries point at remote streamable-HTTP servers.
|
|
43
|
+
* Supported on the claude and codex harnesses, not on pi.
|
|
44
|
+
*/
|
|
45
|
+
mcp?: Record<string, McpServerConfig>;
|
|
46
|
+
/** Which runtime executes this agent. Default: "claude". */
|
|
47
|
+
harness?: HarnessId;
|
|
48
|
+
}
|
|
49
|
+
export declare function defineAgent(agent: AgentDefinition): AgentDefinition;
|
package/dist/agent.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `kraftwerk create "<spec>"` — printed for an LLM agent (Claude Code,
|
|
3
|
+
* Codex, ...), veloop-style: the command does not scaffold anything itself,
|
|
4
|
+
* it emits a self-contained brief the agent follows end to end with the
|
|
5
|
+
* kraftwerk CLI. Everything the agent needs (schema essentials, an example
|
|
6
|
+
* workflow.yml, gates, variables, harness rules, verify ladder) is inline —
|
|
7
|
+
* no other files required reading.
|
|
8
|
+
*/
|
|
9
|
+
import { SCHEMA_URL } from "../config.js";
|
|
10
|
+
export function renderCreateBrief({ spec, workflowsRoot, }) {
|
|
11
|
+
const root = workflowsRoot ?? "src/workflows";
|
|
12
|
+
return `# Create a new kraftwerk workflow
|
|
13
|
+
|
|
14
|
+
**Requirements:** ${spec}
|
|
15
|
+
|
|
16
|
+
You are scaffolding a YAML workflow for kraftwerk. A workflow is a
|
|
17
|
+
FOLDER \`${root}/<name>/\` containing \`workflow.yml\` (agents + gated steps)
|
|
18
|
+
and \`prompts/*.md\`. Deterministic code owns the control flow; agents work
|
|
19
|
+
inside bounded steps and are judged afterwards (envelope + file gates, with
|
|
20
|
+
in-session correction). The kraftwerk CLI discovers the folder automatically —
|
|
21
|
+
nothing to register.
|
|
22
|
+
|
|
23
|
+
## Steps
|
|
24
|
+
|
|
25
|
+
1. Orient: run \`kraftwerk list\` to see existing workflows (avoid name
|
|
26
|
+
collisions, match local conventions).${workflowsRoot ? "" : ` No workflows root exists yet —
|
|
27
|
+
create \`src/workflows/\` first.`}
|
|
28
|
+
2. Design from the requirements — keep it minimal and concrete:
|
|
29
|
+
- **Steps** in order, two kinds: agent steps (\`agent\` + \`prompt\`) and
|
|
30
|
+
deterministic script steps (\`run\`: a bash script — use these whenever
|
|
31
|
+
no judgment is needed: fetching, measuring, converting). Only linear
|
|
32
|
+
sequences fit YAML; if the requirements demand loops or human approval
|
|
33
|
+
gates, STOP and tell the user this needs a TypeScript workflow instead.
|
|
34
|
+
- **Agents**: one per role — persona (WHO), model + optional effort (WHAT
|
|
35
|
+
thinks), tools (governance), \`runs-on\` (WHERE: claude | codex | pi).
|
|
36
|
+
- **Gates** per step: what file evidence proves the step worked?
|
|
37
|
+
3. Write \`${root}/<name>/workflow.yml\`. Complete example of every feature:
|
|
38
|
+
|
|
39
|
+
\`\`\`yaml
|
|
40
|
+
# yaml-language-server: $schema=${SCHEMA_URL}
|
|
41
|
+
name: tagline # CLI name: kraftwerk run tagline "..."
|
|
42
|
+
description: "One-liner shown in kraftwerk list"
|
|
43
|
+
requires: [BRAND_API_TOKEN] # optional: env vars checked before the run starts
|
|
44
|
+
workspace: |
|
|
45
|
+
Files: brand.md (analysis), tagline.md (result).
|
|
46
|
+
mcp: # optional: MCP servers stored with the workflow
|
|
47
|
+
calculator:
|
|
48
|
+
command: node # stdio server; relative files resolve in the folder
|
|
49
|
+
args: [mcp/multiply-server.ts]
|
|
50
|
+
linear:
|
|
51
|
+
url: https://mcp.linear.app/mcp # remote streamable HTTP
|
|
52
|
+
clis: # optional: CLI grants, command prefix -> usage hint
|
|
53
|
+
git: "Version control: commit after every step"
|
|
54
|
+
agents:
|
|
55
|
+
analyst:
|
|
56
|
+
name: Brand analyst # display name (optional)
|
|
57
|
+
model: haiku # model id in the harness's naming
|
|
58
|
+
tools: [Read, Write, Edit, WebFetch]
|
|
59
|
+
persona: |
|
|
60
|
+
You analyze brands based on their website ...
|
|
61
|
+
writer:
|
|
62
|
+
runs-on: codex # optional: claude (default) | codex | pi
|
|
63
|
+
model: gpt-5.6-sol
|
|
64
|
+
effort: high # optional: low | medium | high | xhigh | max
|
|
65
|
+
tools: [Read, Write, Edit]
|
|
66
|
+
clis: [git] # optional: CLI grant — hint lands in the persona
|
|
67
|
+
mcp: [calculator] # optional: MCP grant (governance, like tools)
|
|
68
|
+
persona: prompts/writer-persona.md # single line = file in this folder
|
|
69
|
+
steps:
|
|
70
|
+
- name: measure # deterministic step: bash, no agent
|
|
71
|
+
run: scripts/measure.sh # single line = file; or inline multiline bash
|
|
72
|
+
gates:
|
|
73
|
+
- file_non_empty: metrics.md
|
|
74
|
+
- name: analyze
|
|
75
|
+
agent: analyst
|
|
76
|
+
prompt: prompts/analyze.md # or inline multiline text
|
|
77
|
+
gates:
|
|
78
|
+
- file_non_empty: brand.md
|
|
79
|
+
- contains: { file: brand.md, text: "## Tone of voice", label: tone }
|
|
80
|
+
- name: write
|
|
81
|
+
agent: writer
|
|
82
|
+
prompt: prompts/write.md
|
|
83
|
+
gates:
|
|
84
|
+
- file_non_empty: tagline.md
|
|
85
|
+
- slots_filled: tagline.md # no unfilled {{...}} slots
|
|
86
|
+
\`\`\`
|
|
87
|
+
|
|
88
|
+
4. Write the \`prompts/*.md\` files. Rules:
|
|
89
|
+
- Available variables: \`\${{ request }}\` (the CLI argument) and
|
|
90
|
+
\`\${{ agent }}\` (the step's agent id — lets several steps share one
|
|
91
|
+
prompt file, e.g. writing \`verdict-\${{ agent }}.md\`).
|
|
92
|
+
- Tell the agent exactly which files to read and write (relative names —
|
|
93
|
+
they land in the run directory). The \`workspace:\` text must describe
|
|
94
|
+
the file layout, because steps on different harnesses share state only
|
|
95
|
+
through these files.
|
|
96
|
+
- Do NOT mention envelopes — the engine appends that contract itself.
|
|
97
|
+
Script steps (\`run:\`) execute with bash in the run directory: env vars
|
|
98
|
+
\`REQUEST\`, \`RUN_DIR\`, \`PHASE\` are set and \`\${{ request }}\` is
|
|
99
|
+
interpolated. Non-zero exit fails the run; gates apply but there is no
|
|
100
|
+
correction loop (fix the script). A script MAY end its stdout with the
|
|
101
|
+
same fenced \`\`\`json envelope agents emit (\`{"phase": "$PHASE",
|
|
102
|
+
"status": "ok", "artifacts": [...], "summary": "..."}\`); otherwise the
|
|
103
|
+
engine synthesizes one. Keep scripts in \`scripts/*.sh\` inside the folder.
|
|
104
|
+
5. MCP servers (only if the requirements need custom tools): store the
|
|
105
|
+
server next to the workflow (e.g. \`mcp/multiply-server.ts\` using
|
|
106
|
+
\`@modelcontextprotocol/sdk\` — its deps must be in the consumer's
|
|
107
|
+
package.json; \`command: node\` runs TypeScript directly on node >= 24),
|
|
108
|
+
declare it under top-level \`mcp:\`, grant it per agent via
|
|
109
|
+
\`mcp: [name]\`. External servers: absolute \`command\` path or
|
|
110
|
+
\`url:\` for remote streamable HTTP. Works on claude and codex;
|
|
111
|
+
\`runs-on: pi\` rejects MCP at validation time.
|
|
112
|
+
For EXISTING command-line tools use \`clis:\` instead of an MCP
|
|
113
|
+
server: top-level map command prefix -> one-line usage hint, granted
|
|
114
|
+
per agent via \`clis: [name]\`. The hint is injected into the persona
|
|
115
|
+
once — NEVER repeat CLI usage in step prompts. claude scopes its Bash
|
|
116
|
+
allowlist to \`Bash(<name>:*)\`; codex runs commands in its sandbox
|
|
117
|
+
anyway; pi gets the plain bash tool.
|
|
118
|
+
6. Harness rules:
|
|
119
|
+
- **claude** (default): any Claude model id; needs only the local login.
|
|
120
|
+
- **codex**: ChatGPT login; models from the codex line (e.g.
|
|
121
|
+
\`gpt-5.6-sol\`); a WebFetch/WebSearch grant in \`tools\` enables sandbox
|
|
122
|
+
network access; persona is prepended to the prompt (no system-prompt flag).
|
|
123
|
+
- **pi**: \`provider/id\` models (\`anthropic/...\` uses the Claude login;
|
|
124
|
+
\`deepseek/...\`, \`openrouter/...\` need the vendor key — check with
|
|
125
|
+
\`pi auth check --provider <p>\`).
|
|
126
|
+
- Expensive models only where judgment matters; cheap models elsewhere.
|
|
127
|
+
7. Validate: \`kraftwerk validate ${root}/<name>\` — fix until it passes
|
|
128
|
+
(strict schema: unknown keys are errors; semantic checks cover agent
|
|
129
|
+
references, variables, referenced files).
|
|
130
|
+
8. Smoke: \`kraftwerk run <name> "<realistic request>"\` with all agents on a
|
|
131
|
+
cheap model first (\`haiku\`, or codex which is free on subscription) —
|
|
132
|
+
check every gate passes and the summary table renders. Then set the
|
|
133
|
+
intended models.
|
|
134
|
+
9. Confirm to the user: workflow name, steps, roster (model/harness per
|
|
135
|
+
agent), gates, and the artifact files a run produces.
|
|
136
|
+
|
|
137
|
+
## Notes
|
|
138
|
+
|
|
139
|
+
- Keep it minimal — only the steps/agents/gates the requirements actually
|
|
140
|
+
call for. One agent serving several steps is normal (same persona, new
|
|
141
|
+
task); several agents sharing one prompt file via \`\${{ agent }}\` too.
|
|
142
|
+
- Gates verify claims post-execution, never predictions. Prefer several
|
|
143
|
+
small gates with precise failure texts (they drive the correction loop).
|
|
144
|
+
- If anything essential is unclear (target audience, output files, model
|
|
145
|
+
budget), ask the user before writing files.`;
|
|
146
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runDoctor(cwd: string): Promise<void>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { resolveProject } from "../config.js";
|
|
5
|
+
import { discoverWorkflows } from "../discover.js";
|
|
6
|
+
import { missingEnv } from "../yaml.js";
|
|
7
|
+
const ICONS = {
|
|
8
|
+
ok: chalk.green("✔"),
|
|
9
|
+
warn: chalk.yellow("⚠"),
|
|
10
|
+
fail: chalk.red("✖"),
|
|
11
|
+
info: chalk.dim("•"),
|
|
12
|
+
};
|
|
13
|
+
function report(level, label, detail) {
|
|
14
|
+
console.log(`${ICONS[level]} ${label}${detail ? chalk.dim(` — ${detail}`) : ""}`);
|
|
15
|
+
}
|
|
16
|
+
function cliVersion(command) {
|
|
17
|
+
const r = spawnSync(command, ["--version"], { encoding: "utf8", timeout: 10_000 });
|
|
18
|
+
if (r.status !== 0)
|
|
19
|
+
return undefined;
|
|
20
|
+
return (r.stdout || r.stderr).trim().split("\n")[0];
|
|
21
|
+
}
|
|
22
|
+
export async function runDoctor(cwd) {
|
|
23
|
+
let failures = 0;
|
|
24
|
+
// Runtime.
|
|
25
|
+
const [major] = process.versions.node.split(".").map(Number);
|
|
26
|
+
if (major >= 20)
|
|
27
|
+
report("ok", `node ${process.versions.node}`);
|
|
28
|
+
else {
|
|
29
|
+
report("fail", `node ${process.versions.node}`, "kraftwerk needs node >= 20");
|
|
30
|
+
failures++;
|
|
31
|
+
}
|
|
32
|
+
// Project.
|
|
33
|
+
const project = await resolveProject(cwd);
|
|
34
|
+
report("info", `Project root: ${project.root}`, project.configPath ? path.basename(project.configPath) : "no kraftwerk.yml (fallback: workflows folder or .git)");
|
|
35
|
+
const found = project.workflowsRoot ? await discoverWorkflows(cwd) : [];
|
|
36
|
+
if (!project.workflowsRoot) {
|
|
37
|
+
report("warn", "no workflows root", "expected src/workflows/ or workflows/ — `kraftwerk init` scaffolds one");
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
const valid = found.filter((e) => e.workflow);
|
|
41
|
+
const broken = found.filter((e) => !e.workflow);
|
|
42
|
+
report(broken.length ? "fail" : "ok", `${valid.length} workflow(s) valid, ${broken.length} broken`, path.relative(cwd, project.workflowsRoot) || ".");
|
|
43
|
+
for (const b of broken) {
|
|
44
|
+
report("fail", path.basename(b.path), b.error?.split("\n")[0]);
|
|
45
|
+
failures++;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Harnesses: hard requirement only if a discovered workflow runs on them.
|
|
49
|
+
const needed = new Set();
|
|
50
|
+
for (const e of found) {
|
|
51
|
+
for (const a of e.workflow?.meta.agents ?? [])
|
|
52
|
+
needed.add(a.harness ?? "claude");
|
|
53
|
+
}
|
|
54
|
+
for (const harness of ["claude", "codex", "pi"]) {
|
|
55
|
+
const version = cliVersion(harness);
|
|
56
|
+
const isNeeded = needed.has(harness);
|
|
57
|
+
if (version) {
|
|
58
|
+
report("ok", `${harness} CLI`, version);
|
|
59
|
+
}
|
|
60
|
+
else if (isNeeded) {
|
|
61
|
+
report("fail", `${harness} CLI missing`, "needed by a discovered workflow");
|
|
62
|
+
failures++;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
report("info", `${harness} CLI not installed`, "no discovered workflow needs it");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Docker (only needed for --sandbox).
|
|
69
|
+
const docker = spawnSync("docker", ["info"], { stdio: "ignore", timeout: 15_000 });
|
|
70
|
+
report(docker.status === 0 ? "ok" : "info", docker.status === 0 ? "docker reachable" : "docker not reachable", docker.status === 0 ? undefined : "only needed for --sandbox");
|
|
71
|
+
// Declared env vars.
|
|
72
|
+
for (const e of found) {
|
|
73
|
+
const requires = e.workflow?.meta.requires ?? [];
|
|
74
|
+
if (requires.length === 0)
|
|
75
|
+
continue;
|
|
76
|
+
const missing = missingEnv(requires);
|
|
77
|
+
if (missing.length === 0)
|
|
78
|
+
report("ok", `${e.workflow.name}: requires satisfied`, requires.join(", "));
|
|
79
|
+
else
|
|
80
|
+
report("warn", `${e.workflow.name}: env missing`, missing.join(", "));
|
|
81
|
+
}
|
|
82
|
+
if (failures > 0) {
|
|
83
|
+
console.log(chalk.red(`\n${failures} problem(s) found.`));
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
console.log(chalk.green("\nAll set."));
|
|
87
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runInit(cwd: string): Promise<void>;
|