@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.
Files changed (53) hide show
  1. package/README.md +339 -0
  2. package/bin/kraftwerk.js +24 -0
  3. package/dist/agent.d.ts +49 -0
  4. package/dist/agent.js +3 -0
  5. package/dist/cli/create-brief.d.ts +4 -0
  6. package/dist/cli/create-brief.js +146 -0
  7. package/dist/cli/doctor.d.ts +1 -0
  8. package/dist/cli/doctor.js +87 -0
  9. package/dist/cli/init.d.ts +1 -0
  10. package/dist/cli/init.js +81 -0
  11. package/dist/cli/kraftwerk.d.ts +1 -0
  12. package/dist/cli/kraftwerk.js +342 -0
  13. package/dist/cli/runs.d.ts +6 -0
  14. package/dist/cli/runs.js +120 -0
  15. package/dist/cli.d.ts +13 -0
  16. package/dist/cli.js +47 -0
  17. package/dist/config.d.ts +41 -0
  18. package/dist/config.js +97 -0
  19. package/dist/discover.d.ts +21 -0
  20. package/dist/discover.js +38 -0
  21. package/dist/envelope.d.ts +21 -0
  22. package/dist/envelope.js +61 -0
  23. package/dist/gates.d.ts +18 -0
  24. package/dist/gates.js +34 -0
  25. package/dist/harness.d.ts +66 -0
  26. package/dist/harness.js +14 -0
  27. package/dist/harnesses/claude.d.ts +2 -0
  28. package/dist/harnesses/claude.js +117 -0
  29. package/dist/harnesses/codex.d.ts +2 -0
  30. package/dist/harnesses/codex.js +158 -0
  31. package/dist/harnesses/pi.d.ts +2 -0
  32. package/dist/harnesses/pi.js +151 -0
  33. package/dist/harnesses/registry.d.ts +2 -0
  34. package/dist/harnesses/registry.js +19 -0
  35. package/dist/index.d.ts +23 -0
  36. package/dist/index.js +22 -0
  37. package/dist/remote.d.ts +23 -0
  38. package/dist/remote.js +57 -0
  39. package/dist/run.d.ts +66 -0
  40. package/dist/run.js +278 -0
  41. package/dist/runner/docker.d.ts +38 -0
  42. package/dist/runner/docker.js +166 -0
  43. package/dist/stats.d.ts +46 -0
  44. package/dist/stats.js +65 -0
  45. package/dist/validate.d.ts +8 -0
  46. package/dist/validate.js +33 -0
  47. package/dist/workflow.d.ts +24 -0
  48. package/dist/workflow.js +11 -0
  49. package/dist/yaml.d.ts +21 -0
  50. package/dist/yaml.js +324 -0
  51. package/package.json +52 -0
  52. package/runner/Dockerfile +43 -0
  53. 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.
@@ -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
+ }
@@ -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,3 @@
1
+ export function defineAgent(agent) {
2
+ return agent;
3
+ }
@@ -0,0 +1,4 @@
1
+ export declare function renderCreateBrief({ spec, workflowsRoot, }: {
2
+ spec: string;
3
+ workflowsRoot?: string;
4
+ }): string;
@@ -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>;