@netnodeag/kraftwerk 0.6.0 → 0.8.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 +27 -2
- package/dist/cli/doctor.js +40 -3
- package/dist/cli/init.js +66 -15
- package/dist/cli/kraftwerk.js +9 -10
- package/dist/cli/runs.js +13 -6
- package/dist/cli/ui.js +3 -4
- package/dist/config.d.ts +6 -0
- package/dist/config.js +4 -2
- package/dist/harnesses/claude.js +0 -1
- package/dist/inspector/chat/acp.js +10 -2
- package/dist/inspector/chat/backend.d.ts +6 -0
- package/dist/inspector/chat/pi.js +1 -0
- package/dist/inspector/chat/sessions.d.ts +4 -0
- package/dist/inspector/chat/sessions.js +105 -14
- package/dist/inspector/context.d.ts +7 -2
- package/dist/inspector/context.js +11 -3
- package/dist/inspector/runner.d.ts +10 -0
- package/dist/inspector/runner.js +6 -8
- package/dist/inspector/runs.d.ts +2 -0
- package/dist/inspector/runs.js +17 -12
- package/dist/inspector/server.d.ts +2 -0
- package/dist/inspector/server.js +40 -2
- package/dist/inspector/skills.d.ts +26 -0
- package/dist/inspector/skills.js +65 -0
- package/dist/inspector/team.d.ts +7 -0
- package/dist/inspector/team.js +2 -0
- package/dist/runner/docker.d.ts +3 -1
- package/dist/runner/docker.js +5 -5
- package/dist/workflow.d.ts +7 -0
- package/dist/workflow.js +19 -0
- package/dist/yaml.js +2 -2
- package/inspector/dist/assets/index-Bs8rMhqu.js +13 -0
- package/inspector/dist/assets/index-C8AKqc_i.css +1 -0
- package/inspector/dist/assets/roboto-latin-400-normal-BqEyEoaF.woff2 +0 -0
- package/inspector/dist/assets/roboto-latin-400-normal-D27F5sTu.woff +0 -0
- package/inspector/dist/assets/roboto-latin-500-normal-7RbcRiD8.woff2 +0 -0
- package/inspector/dist/assets/roboto-latin-500-normal-fsTFPL3E.woff +0 -0
- package/inspector/dist/assets/roboto-latin-700-normal-BZpUvMxY.woff2 +0 -0
- package/inspector/dist/assets/roboto-latin-700-normal-YuyVweIx.woff +0 -0
- package/inspector/dist/assets/roboto-mono-latin-400-normal-DBZPkcnn.woff +0 -0
- package/inspector/dist/assets/roboto-mono-latin-400-normal-GekRknry.woff2 +0 -0
- package/inspector/dist/assets/roboto-mono-latin-500-normal-D6j6AMzd.woff +0 -0
- package/inspector/dist/assets/roboto-mono-latin-500-normal-pxIOXZmA.woff2 +0 -0
- package/inspector/dist/assets/roboto-mono-latin-700-normal-CHYrj1DZ.woff +0 -0
- package/inspector/dist/assets/roboto-mono-latin-700-normal-foxDnrFi.woff2 +0 -0
- package/inspector/dist/index.html +2 -2
- package/package.json +1 -1
- package/inspector/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-700-normal-7sUh57Bg.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-700-normal-CNHXzs6v.woff +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-700-normal-Bth3BMcD.woff +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-700-normal-Bxkt5Cjx.woff2 +0 -0
- package/inspector/dist/assets/index-BtcPa8nx.css +0 -1
- package/inspector/dist/assets/index-CImcmBVs.js +0 -10
package/README.md
CHANGED
|
@@ -153,6 +153,7 @@ model: sonnet # optional; harness default when omitted
|
|
|
153
153
|
effort: medium # optional: low | medium | high | xhigh | max
|
|
154
154
|
workflows: [tagline, website-check]
|
|
155
155
|
knowledge: [customer-support] # OKF bundles the member consults & maintains
|
|
156
|
+
skills: [report-html] # optional allowlist; omit = all skills, [] = none
|
|
156
157
|
```
|
|
157
158
|
|
|
158
159
|
Sessions with a member are ordinary chats scoped `{ kind: "team", member }`,
|
|
@@ -169,6 +170,29 @@ options and the thinking budget via `MAX_THINKING_TOKENS`; codex gets a
|
|
|
169
170
|
`--model`/`--thinking` flags. Members are created and edited in the UI (or by
|
|
170
171
|
editing the files — the definition is read fresh for each new session).
|
|
171
172
|
|
|
173
|
+
### Skills in chat
|
|
174
|
+
|
|
175
|
+
Chats — general ones and team sessions — can use skills: Claude-style
|
|
176
|
+
instruction packages, one folder per skill with a `SKILL.md` (YAML
|
|
177
|
+
frontmatter `name` + `description`, then the instructions). Two roots are
|
|
178
|
+
discovered, with a project skill shadowing a same-named user skill:
|
|
179
|
+
|
|
180
|
+
```
|
|
181
|
+
<project>/.claude/skills/<name>/SKILL.md # git-tracked, per project
|
|
182
|
+
~/.claude/skills/<name>/SKILL.md # personal, per user
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Every chat lists its visible skills as context ("## Your skills"), so the
|
|
186
|
+
agent reaches for one when the request matches. Typing `/` in the composer
|
|
187
|
+
opens an autocomplete over them; sending `/<name> <args>` expands the
|
|
188
|
+
skill's SKILL.md into the prompt — which is why this works identically on
|
|
189
|
+
claude, codex, and pi. Natively on top of that, claude discovers
|
|
190
|
+
`.claude/skills` itself (a team member's `skills:` allowlist narrows that
|
|
191
|
+
via ACP session options) and pi loads each visible skill folder via
|
|
192
|
+
`--skill`. Team members take an optional `skills:` list in `agent.yml`:
|
|
193
|
+
omitted means all discovered skills, an empty list means none, otherwise
|
|
194
|
+
it's the allowlist. `GET /api/skills` returns what's discovered.
|
|
195
|
+
|
|
172
196
|
### Routines — scheduled prompts
|
|
173
197
|
|
|
174
198
|
A member can have routines: cron-scheduled prompts, like standing orders for
|
|
@@ -405,8 +429,9 @@ Living examples:
|
|
|
405
429
|
[`../agent-playground/src/workflows/tagline/`](../agent-playground/src/workflows/tagline/)
|
|
406
430
|
and [`../agent-playground/src/workflows/pitch/`](../agent-playground/src/workflows/pitch/);
|
|
407
431
|
`check`/`if` in [`../agent-playground/src/workflows/helpdesk-check/`](../agent-playground/src/workflows/helpdesk-check/).
|
|
408
|
-
v1 is deliberately linear — approval loops
|
|
409
|
-
|
|
432
|
+
v1 is deliberately linear — approval loops and AGENTS.md-style context files
|
|
433
|
+
stay on the roadmap (skills exist in chat, not in workflow runs); anything
|
|
434
|
+
non-linear is a TS workflow.
|
|
410
435
|
|
|
411
436
|
### MCP servers alongside the workflow
|
|
412
437
|
|
package/dist/cli/doctor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { resolveProject } from "../config.js";
|
|
@@ -67,9 +67,46 @@ export async function runDoctor(cwd) {
|
|
|
67
67
|
else {
|
|
68
68
|
report("ok", `kraftwerk ${pkg.version}`, latest === pkg.version ? "latest on npm" : `ahead of npm (latest: ${latest})`);
|
|
69
69
|
}
|
|
70
|
-
// Project.
|
|
71
|
-
|
|
70
|
+
// Project. resolveProject throws on a malformed kraftwerk.yml (bad YAML,
|
|
71
|
+
// non-mapping, unknown keys, non-string values) — surface that as a check.
|
|
72
|
+
let project;
|
|
73
|
+
try {
|
|
74
|
+
project = await resolveProject(cwd);
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
report("fail", "kraftwerk.yml invalid", err.message.split("\n")[0]);
|
|
78
|
+
console.log(chalk.red("\n1 problem(s) found."));
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
72
81
|
report("info", `Project root: ${project.root}`, project.configPath ? path.basename(project.configPath) : "no kraftwerk.yml (fallback: workflows folder or .git)");
|
|
82
|
+
// Project config: well-formed (guaranteed by the parse above) and the
|
|
83
|
+
// configured paths actually exist.
|
|
84
|
+
if (!project.configPath) {
|
|
85
|
+
report("warn", "no kraftwerk.yml", "recommended as project root marker — `kraftwerk init` scaffolds one");
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
const cfg = project.config;
|
|
89
|
+
const keys = Object.keys(cfg);
|
|
90
|
+
report("ok", `${path.basename(project.configPath)} well-formed`, keys.length ? keys.map((k) => `${k}: ${cfg[k]}`).join(", ") : "empty — defaults apply");
|
|
91
|
+
if (!cfg.name)
|
|
92
|
+
report("info", "name not set in kraftwerk.yml", "inspector header falls back to the folder name");
|
|
93
|
+
const isDir = async (p) => (await stat(p).catch(() => null))?.isDirectory() ?? false;
|
|
94
|
+
for (const key of ["workflows", "knowledge", "agents", "output"]) {
|
|
95
|
+
const value = cfg[key];
|
|
96
|
+
if (!value)
|
|
97
|
+
continue;
|
|
98
|
+
const abs = path.resolve(project.root, value);
|
|
99
|
+
if (await isDir(abs))
|
|
100
|
+
continue;
|
|
101
|
+
if (key === "output") {
|
|
102
|
+
report("info", `output: ${value}`, "directory not created yet — appears on first run");
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
report("fail", `${key}: ${value}`, "configured directory does not exist");
|
|
106
|
+
failures++;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
73
110
|
const found = project.workflowsRoot ? await discoverWorkflows(cwd) : [];
|
|
74
111
|
if (!project.workflowsRoot) {
|
|
75
112
|
report("warn", "no workflows root", "expected src/workflows/ or workflows/ — `kraftwerk init` scaffolds one");
|
package/dist/cli/init.js
CHANGED
|
@@ -2,16 +2,23 @@ import { appendFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import { SCHEMA_URL } from "../config.js";
|
|
5
|
+
import { initBundle, writeConcept } from "../okf.js";
|
|
5
6
|
/**
|
|
6
7
|
* `kraftwerk init` — make any repository a kraftwerk consumer in one
|
|
7
|
-
* command: kraftwerk.yml (root marker + config),
|
|
8
|
-
* example,
|
|
9
|
-
*
|
|
8
|
+
* command: kraftwerk.yml (root marker + config), a kraftwerk-data/ tree with
|
|
9
|
+
* a runnable example workflow, a starter team agent ("max"), a demo
|
|
10
|
+
* knowledge bundle, and an output ignore entry. Everything is idempotent:
|
|
11
|
+
* existing files are left untouched and reported.
|
|
10
12
|
*/
|
|
11
|
-
const
|
|
13
|
+
const DATA_DIR = "kraftwerk-data";
|
|
14
|
+
const configTemplate = (name) => `# kraftwerk project config — also marks the project root for the CLI.
|
|
12
15
|
# All fields optional. Docs: https://github.com/NETNODEAG/kraftwerk
|
|
13
|
-
|
|
14
|
-
|
|
16
|
+
name: ${JSON.stringify(name)} # display name, shown in the inspector header + browser tab
|
|
17
|
+
icon: "⚡" # emoji shown as the inspector favicon
|
|
18
|
+
workflows: ${DATA_DIR}/workflows # where workflows live
|
|
19
|
+
output: ${DATA_DIR}/output # where run artifacts land (git-ignored)
|
|
20
|
+
knowledge: ${DATA_DIR}/knowledge # OKF knowledge bundles
|
|
21
|
+
agents: ${DATA_DIR}/agents # team agent definitions
|
|
15
22
|
`;
|
|
16
23
|
const WORKFLOW_TEMPLATE = `# yaml-language-server: $schema=${SCHEMA_URL}
|
|
17
24
|
name: hello
|
|
@@ -38,6 +45,34 @@ const PROMPT_TEMPLATE = `Request: \${{ request }}
|
|
|
38
45
|
Answer the request and write the answer to answer.md
|
|
39
46
|
(Markdown, with a short heading).
|
|
40
47
|
`;
|
|
48
|
+
const AGENT_YML_TEMPLATE = `name: Max - Workflow Runner
|
|
49
|
+
emoji: 🛠️
|
|
50
|
+
description: Runs and explains this project's workflows
|
|
51
|
+
harness: claude
|
|
52
|
+
model: sonnet
|
|
53
|
+
effort: medium
|
|
54
|
+
workflows:
|
|
55
|
+
- hello
|
|
56
|
+
knowledge:
|
|
57
|
+
- demo-customer-support
|
|
58
|
+
`;
|
|
59
|
+
const AGENT_SYSTEM_TEMPLATE = `You are Max, this project's workflow runner. You run and explain the
|
|
60
|
+
project's kraftwerk workflows, and you consult the knowledge bundles you
|
|
61
|
+
have access to before answering questions they cover.
|
|
62
|
+
`;
|
|
63
|
+
const DEMO_BUNDLE = "demo-customer-support";
|
|
64
|
+
const DEMO_CONCEPT = `---
|
|
65
|
+
type: Playbook
|
|
66
|
+
title: Refund handling
|
|
67
|
+
description: How support handles refund requests.
|
|
68
|
+
tags: [ support, refunds ]
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
# Steps
|
|
72
|
+
|
|
73
|
+
1. Check the order in the shop backend.
|
|
74
|
+
2. Refunds under CHF 50 are approved directly.
|
|
75
|
+
`;
|
|
41
76
|
export async function runInit(cwd) {
|
|
42
77
|
const exists = async (p) => !!(await stat(p).catch(() => null));
|
|
43
78
|
const created = [];
|
|
@@ -52,20 +87,36 @@ export async function runInit(cwd) {
|
|
|
52
87
|
await writeFile(abs, content);
|
|
53
88
|
created.push(rel);
|
|
54
89
|
};
|
|
55
|
-
await put("kraftwerk.yml",
|
|
56
|
-
await put(
|
|
57
|
-
await put(
|
|
58
|
-
await put(
|
|
59
|
-
//
|
|
90
|
+
await put("kraftwerk.yml", configTemplate(path.basename(path.resolve(cwd))));
|
|
91
|
+
await put(`${DATA_DIR}/workflows/hello/workflow.yml`, WORKFLOW_TEMPLATE);
|
|
92
|
+
await put(`${DATA_DIR}/workflows/hello/prompts/assistant.md`, PERSONA_TEMPLATE);
|
|
93
|
+
await put(`${DATA_DIR}/workflows/hello/prompts/answer.md`, PROMPT_TEMPLATE);
|
|
94
|
+
// Team agent "max".
|
|
95
|
+
await put(`${DATA_DIR}/agents/max/agent.yml`, AGENT_YML_TEMPLATE);
|
|
96
|
+
await put(`${DATA_DIR}/agents/max/system.md`, AGENT_SYSTEM_TEMPLATE);
|
|
97
|
+
// Demo knowledge bundle, through the canonical OKF write path so
|
|
98
|
+
// index.md and log.md come out consistent.
|
|
99
|
+
const knowledgeRoot = path.join(cwd, DATA_DIR, "knowledge");
|
|
100
|
+
const bundleRel = `${DATA_DIR}/knowledge/${DEMO_BUNDLE}/`;
|
|
101
|
+
if (await exists(path.join(knowledgeRoot, DEMO_BUNDLE))) {
|
|
102
|
+
skipped.push(bundleRel);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
await initBundle(knowledgeRoot, DEMO_BUNDLE);
|
|
106
|
+
await writeConcept(knowledgeRoot, DEMO_BUNDLE, "playbooks/refunds", DEMO_CONCEPT, "kraftwerk-init");
|
|
107
|
+
created.push(bundleRel);
|
|
108
|
+
}
|
|
109
|
+
// .gitignore: append the output dir if it's not covered yet.
|
|
110
|
+
const outputEntry = `${DATA_DIR}/output`;
|
|
60
111
|
const gitignorePath = path.join(cwd, ".gitignore");
|
|
61
112
|
const gitignore = (await readFile(gitignorePath, "utf8").catch(() => null)) ?? null;
|
|
62
113
|
if (gitignore === null) {
|
|
63
|
-
await writeFile(gitignorePath,
|
|
114
|
+
await writeFile(gitignorePath, `${outputEntry}/\n`);
|
|
64
115
|
created.push(".gitignore");
|
|
65
116
|
}
|
|
66
|
-
else if (!gitignore.split("\n").some((l) => l.trim().replace(/\/$/, "") ===
|
|
67
|
-
await appendFile(gitignorePath, `${gitignore.endsWith("\n") ? "" : "\n"}
|
|
68
|
-
created.push(
|
|
117
|
+
else if (!gitignore.split("\n").some((l) => l.trim().replace(/\/$/, "") === outputEntry)) {
|
|
118
|
+
await appendFile(gitignorePath, `${gitignore.endsWith("\n") ? "" : "\n"}${outputEntry}/\n`);
|
|
119
|
+
created.push(`.gitignore (${outputEntry}/ added)`);
|
|
69
120
|
}
|
|
70
121
|
else {
|
|
71
122
|
skipped.push(".gitignore");
|
package/dist/cli/kraftwerk.js
CHANGED
|
@@ -17,7 +17,7 @@ import { runUi } from "./ui.js";
|
|
|
17
17
|
/**
|
|
18
18
|
* kraftwerk — the kraftwerk CLI.
|
|
19
19
|
*
|
|
20
|
-
* kraftwerk init scaffold kraftwerk.yml +
|
|
20
|
+
* kraftwerk init scaffold kraftwerk.yml + kraftwerk-data/ (workflow, agent, knowledge)
|
|
21
21
|
* kraftwerk list discover + list workflows (--json, --from)
|
|
22
22
|
* kraftwerk run [workflow] [text] run one (prompts interactively if omitted)
|
|
23
23
|
* kraftwerk runs [show <id>] inspect past runs from their traces
|
|
@@ -129,7 +129,7 @@ program
|
|
|
129
129
|
.option("--from <source>", "Workflows from a git remote: github:org/repo[@ref] or git URL")
|
|
130
130
|
.option("--sandbox", "Run inside the Docker sandbox container (image: kraftwerk-runner)")
|
|
131
131
|
.option("--ssh", "Forward the SSH agent + known_hosts into the sandbox (only with --sandbox)")
|
|
132
|
-
.option("--run-id <id>", "Pin the run folder name (output/<id>), e.g. for external triggers")
|
|
132
|
+
.option("--run-id <id>", "Pin the run folder name (output/runs/<id>), e.g. for external triggers")
|
|
133
133
|
.action(async (name, requestParts, opts) => {
|
|
134
134
|
const machine = !!opts.json;
|
|
135
135
|
const fail = (code, message) => {
|
|
@@ -172,8 +172,10 @@ program
|
|
|
172
172
|
}
|
|
173
173
|
if (opts.sandbox) {
|
|
174
174
|
const { runSandboxed } = await import("../runner/docker.js");
|
|
175
|
+
const { resolveProject } = await import("../config.js");
|
|
175
176
|
const handle = await runSandboxed({
|
|
176
177
|
projectRoot: baseDir,
|
|
178
|
+
outputDir: opts.from ? undefined : (await resolveProject(baseDir)).outputDir,
|
|
177
179
|
workflowPath: entry.path,
|
|
178
180
|
workflowName: workflow.name,
|
|
179
181
|
request,
|
|
@@ -184,17 +186,14 @@ program
|
|
|
184
186
|
console.log(chalk.dim(`sandbox ${handle.containerName} → ${handle.runDir}`));
|
|
185
187
|
process.exit(await handle.finished);
|
|
186
188
|
}
|
|
187
|
-
if (opts.runId) {
|
|
188
|
-
process.env.KRAFTWERK_RUN_DIR = path.resolve("output", opts.runId);
|
|
189
|
-
}
|
|
190
|
-
else if (!process.env.KRAFTWERK_RUN_DIR) {
|
|
189
|
+
if (opts.runId || !process.env.KRAFTWERK_RUN_DIR) {
|
|
191
190
|
// Root the run dir explicitly: at the project's output dir (honors
|
|
192
191
|
// kraftwerk.yml `output:` and works from subdirs); for remote runs at
|
|
193
192
|
// the caller's cwd — never inside the clone cache.
|
|
194
|
-
const {
|
|
193
|
+
const { newRunId, runDirFor } = await import("../workflow.js");
|
|
195
194
|
const { resolveProject } = await import("../config.js");
|
|
196
195
|
const outBase = opts.from ? path.resolve("output") : (await resolveProject(baseDir)).outputDir;
|
|
197
|
-
process.env.KRAFTWERK_RUN_DIR =
|
|
196
|
+
process.env.KRAFTWERK_RUN_DIR = runDirFor(outBase, opts.runId ?? newRunId(workflow.name));
|
|
198
197
|
}
|
|
199
198
|
// Machine/quiet mode: workflow narration must not pollute stdout — the
|
|
200
199
|
// engine logs via console.log, so reroute it for the duration of the run.
|
|
@@ -225,7 +224,7 @@ program
|
|
|
225
224
|
});
|
|
226
225
|
program
|
|
227
226
|
.command("init")
|
|
228
|
-
.description("Scaffold the project: kraftwerk.yml,
|
|
227
|
+
.description("Scaffold the project: kraftwerk.yml, kraftwerk-data/ (example workflow, agent max, demo knowledge), .gitignore")
|
|
229
228
|
.action(async () => {
|
|
230
229
|
await runInit(process.cwd());
|
|
231
230
|
});
|
|
@@ -248,7 +247,7 @@ runs
|
|
|
248
247
|
runs
|
|
249
248
|
.command("show")
|
|
250
249
|
.description("Show one run in detail: phases, gates, cost")
|
|
251
|
-
.argument("<runId>", "Folder name under output/, see `kraftwerk runs`")
|
|
250
|
+
.argument("<runId>", "Folder name under output/runs/, see `kraftwerk runs`")
|
|
252
251
|
.option("--json", "Machine-readable output (all trace events)")
|
|
253
252
|
.action(async (runId, opts) => {
|
|
254
253
|
await showRun(process.cwd(), runId, opts);
|
package/dist/cli/runs.js
CHANGED
|
@@ -4,6 +4,7 @@ import chalk from "chalk";
|
|
|
4
4
|
import Table from "cli-table3";
|
|
5
5
|
import { resolveProject } from "../config.js";
|
|
6
6
|
import { fmtDuration, fmtTokens } from "../stats.js";
|
|
7
|
+
import { runDirFor } from "../workflow.js";
|
|
7
8
|
async function readRun(dir) {
|
|
8
9
|
const tracePath = path.join(dir, "trace.jsonl");
|
|
9
10
|
const raw = await readFile(tracePath, "utf8").catch(() => null);
|
|
@@ -37,12 +38,18 @@ async function readRun(dir) {
|
|
|
37
38
|
}
|
|
38
39
|
async function collectRuns(cwd) {
|
|
39
40
|
const { outputDir } = await resolveProject(cwd);
|
|
40
|
-
|
|
41
|
+
// New runs live under <output>/runs/; legacy run-* folders sit at the root.
|
|
42
|
+
const dirs = [
|
|
43
|
+
...(await readdir(path.join(outputDir, "runs"), { withFileTypes: true }).catch(() => []))
|
|
44
|
+
.filter((e) => e.isDirectory())
|
|
45
|
+
.map((e) => path.join(outputDir, "runs", e.name)),
|
|
46
|
+
...(await readdir(outputDir, { withFileTypes: true }).catch(() => []))
|
|
47
|
+
.filter((e) => e.isDirectory() && e.name.startsWith("run-"))
|
|
48
|
+
.map((e) => path.join(outputDir, e.name)),
|
|
49
|
+
];
|
|
41
50
|
const runs = [];
|
|
42
|
-
for (const
|
|
43
|
-
|
|
44
|
-
continue;
|
|
45
|
-
const info = await readRun(path.join(outputDir, entry.name));
|
|
51
|
+
for (const dir of dirs) {
|
|
52
|
+
const info = await readRun(dir);
|
|
46
53
|
if (info)
|
|
47
54
|
runs.push(info);
|
|
48
55
|
}
|
|
@@ -83,7 +90,7 @@ export async function listRuns(cwd, opts = {}) {
|
|
|
83
90
|
}
|
|
84
91
|
export async function showRun(cwd, id, opts = {}) {
|
|
85
92
|
const { outputDir } = await resolveProject(cwd);
|
|
86
|
-
const dir =
|
|
93
|
+
const dir = runDirFor(outputDir, id);
|
|
87
94
|
if (!(await stat(dir).catch(() => null))) {
|
|
88
95
|
console.error(chalk.red(`Run "${id}" not found under ${outputDir}.`));
|
|
89
96
|
process.exit(2);
|
package/dist/cli/ui.js
CHANGED
|
@@ -42,11 +42,10 @@ function ensureBuilt() {
|
|
|
42
42
|
}
|
|
43
43
|
export async function runUi(cwd, opts) {
|
|
44
44
|
const staticDir = ensureBuilt();
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
: (await resolveProject(cwd)).outputDir;
|
|
45
|
+
const project = await resolveProject(cwd);
|
|
46
|
+
const outputDir = opts.output ? path.resolve(cwd, opts.output) : project.outputDir;
|
|
48
47
|
const port = Number(opts.port ?? "1981");
|
|
49
|
-
await startInspector({ outputDir, staticDir, port });
|
|
48
|
+
await startInspector({ outputDir, staticDir, port, projectRoot: project.root });
|
|
50
49
|
console.log(`${chalk.green("✔")} Inspector: ${chalk.cyan(`http://localhost:${port}`)} ` +
|
|
51
50
|
chalk.dim(`(output: ${outputDir})`));
|
|
52
51
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
*
|
|
10
10
|
* All fields are optional — a valid kraftwerk.yml may be empty:
|
|
11
11
|
*
|
|
12
|
+
* name: my-project # display name (inspector header, "environment")
|
|
13
|
+
* icon: "⚡" # emoji shown as inspector favicon
|
|
12
14
|
* workflows: src/workflows # workflows root, relative to the file
|
|
13
15
|
* output: output # run-artifact directory, relative to the file
|
|
14
16
|
* knowledge: knowledge # OKF knowledge-bundle root, relative to the file
|
|
@@ -18,6 +20,10 @@
|
|
|
18
20
|
export declare const SCHEMA_URL = "https://raw.githubusercontent.com/NETNODEAG/kraftwerk/main/kraftwerk/schema/workflow.schema.json";
|
|
19
21
|
export declare const CONFIG_FILENAMES: string[];
|
|
20
22
|
export interface ProjectConfig {
|
|
23
|
+
/** Display name of the project ("environment"), shown in the inspector header. */
|
|
24
|
+
name?: string;
|
|
25
|
+
/** Emoji used as the inspector favicon (browser-tab icon). */
|
|
26
|
+
icon?: string;
|
|
21
27
|
/** Workflows root relative to the project root. */
|
|
22
28
|
workflows?: string;
|
|
23
29
|
/** Run-artifact directory relative to the project root. Default: output */
|
package/dist/config.js
CHANGED
|
@@ -12,6 +12,8 @@ import { parse } from "yaml";
|
|
|
12
12
|
*
|
|
13
13
|
* All fields are optional — a valid kraftwerk.yml may be empty:
|
|
14
14
|
*
|
|
15
|
+
* name: my-project # display name (inspector header, "environment")
|
|
16
|
+
* icon: "⚡" # emoji shown as inspector favicon
|
|
15
17
|
* workflows: src/workflows # workflows root, relative to the file
|
|
16
18
|
* output: output # run-artifact directory, relative to the file
|
|
17
19
|
* knowledge: knowledge # OKF knowledge-bundle root, relative to the file
|
|
@@ -83,10 +85,10 @@ async function loadConfig(configPath) {
|
|
|
83
85
|
if (raw === null || raw === undefined)
|
|
84
86
|
return {};
|
|
85
87
|
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
86
|
-
throw new Error(`${path.basename(configPath)}: expected a mapping (workflows, output, knowledge)`);
|
|
88
|
+
throw new Error(`${path.basename(configPath)}: expected a mapping (name, icon, workflows, output, knowledge, agents)`);
|
|
87
89
|
}
|
|
88
90
|
const config = raw;
|
|
89
|
-
const known = ["workflows", "output", "knowledge", "agents"];
|
|
91
|
+
const known = ["name", "icon", "workflows", "output", "knowledge", "agents"];
|
|
90
92
|
for (const key of Object.keys(config)) {
|
|
91
93
|
if (!known.includes(key)) {
|
|
92
94
|
throw new Error(`${path.basename(configPath)}: unknown key "${key}" (allowed: ${known.join(", ")})`);
|
package/dist/harnesses/claude.js
CHANGED
|
@@ -118,11 +118,19 @@ export async function startAcpBackend(agent, cwd, hooks, tuning = {}) {
|
|
|
118
118
|
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } },
|
|
119
119
|
clientInfo: { name: "kraftwerk-inspector", version: "1.0.0" },
|
|
120
120
|
});
|
|
121
|
+
// Claude-only session options ride on _meta.claudeCode.options (the
|
|
122
|
+
// adapter spreads them into the Agent SDK options): model override,
|
|
123
|
+
// skill allowlist (undefined = all discovered skills), extra work dirs.
|
|
124
|
+
const claudeOptions = {
|
|
125
|
+
...(tuning.model ? { model: tuning.model } : {}),
|
|
126
|
+
...(tuning.skills ? { skills: tuning.skills } : {}),
|
|
127
|
+
...(tuning.addDirs?.length ? { additionalDirectories: tuning.addDirs } : {}),
|
|
128
|
+
};
|
|
121
129
|
const session = await conn.newSession({
|
|
122
130
|
cwd,
|
|
123
131
|
mcpServers: [],
|
|
124
|
-
...(agent === "claude" &&
|
|
125
|
-
? { _meta: { claudeCode: { options:
|
|
132
|
+
...(agent === "claude" && Object.keys(claudeOptions).length
|
|
133
|
+
? { _meta: { claudeCode: { options: claudeOptions } } }
|
|
126
134
|
: {}),
|
|
127
135
|
});
|
|
128
136
|
const sessionId = session.sessionId;
|
|
@@ -25,6 +25,12 @@ export interface BackendHooks {
|
|
|
25
25
|
export interface BackendTuning {
|
|
26
26
|
model?: string;
|
|
27
27
|
effort?: string;
|
|
28
|
+
/** claude: restrict native skill discovery to these names (undefined = all). */
|
|
29
|
+
skills?: string[];
|
|
30
|
+
/** pi: skill folders to load explicitly via --skill. */
|
|
31
|
+
skillDirs?: string[];
|
|
32
|
+
/** claude: extra directories the session may work in (e.g. project root for run chats). */
|
|
33
|
+
addDirs?: string[];
|
|
28
34
|
}
|
|
29
35
|
export interface ChatBackend {
|
|
30
36
|
/** Send one user message; resolves with the stop reason at turn end. */
|
|
@@ -11,6 +11,7 @@ export function startPiBackend(cwd, hooks, tuning = {}) {
|
|
|
11
11
|
const tuningArgs = [
|
|
12
12
|
...(tuning.model ? ["--model", tuning.model] : []),
|
|
13
13
|
...(tuning.effort ? ["--thinking", tuning.effort] : []),
|
|
14
|
+
...(tuning.skillDirs ?? []).flatMap((dir) => ["--skill", dir]),
|
|
14
15
|
];
|
|
15
16
|
return {
|
|
16
17
|
prompt(text) {
|
|
@@ -19,6 +19,10 @@ export declare function postMessage(id: string, text: string): Promise<{
|
|
|
19
19
|
export declare function resolvePermission(id: string, requestId: string, optionId: string | null): Promise<{
|
|
20
20
|
error?: string;
|
|
21
21
|
}>;
|
|
22
|
+
/** Delete a chat: kill its backend, drop the live state, remove its folder. */
|
|
23
|
+
export declare function deleteChat(id: string): Promise<{
|
|
24
|
+
error?: string;
|
|
25
|
+
}>;
|
|
22
26
|
export declare function cancelChat(id: string): Promise<{
|
|
23
27
|
error?: string;
|
|
24
28
|
}>;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { promises as fs } from "node:fs";
|
|
2
3
|
import { getOutputDir, getProjectRoot } from "../context.js";
|
|
3
4
|
import { knowledgeIndex } from "../knowledge.js";
|
|
4
5
|
import { getRun, listRuns, safeRunDir } from "../runs.js";
|
|
6
|
+
import { listSkills, readSkill } from "../skills.js";
|
|
5
7
|
import { listWorkflows } from "../workflows.js";
|
|
6
8
|
import { getMember } from "../team.js";
|
|
7
9
|
import { startAcpBackend } from "./acp.js";
|
|
8
10
|
import { startPiBackend } from "./pi.js";
|
|
9
|
-
import { appendEvent, listChatMetas, newChatId, readEvents, readMeta, writeMeta } from "./store.js";
|
|
11
|
+
import { appendEvent, listChatMetas, newChatId, readEvents, readMeta, safeChatDir, writeMeta, } from "./store.js";
|
|
10
12
|
const states = new Map();
|
|
11
13
|
async function loadState(id) {
|
|
12
14
|
const existing = states.get(id);
|
|
@@ -42,6 +44,54 @@ function emit(state, ev) {
|
|
|
42
44
|
fn(stored);
|
|
43
45
|
return stored;
|
|
44
46
|
}
|
|
47
|
+
/* ---------- skills ---------- */
|
|
48
|
+
/**
|
|
49
|
+
* Skills visible to a chat: every discovered skill (project + user
|
|
50
|
+
* .claude/skills), narrowed by the team member's allowlist when the chat
|
|
51
|
+
* is a team session (absent allowlist = all, empty = none).
|
|
52
|
+
*/
|
|
53
|
+
async function availableSkills(scope) {
|
|
54
|
+
const all = await listSkills().catch(() => []);
|
|
55
|
+
if (scope.kind !== "team" || all.length === 0)
|
|
56
|
+
return all;
|
|
57
|
+
const member = await getMember(scope.member).catch(() => null);
|
|
58
|
+
if (!member || member.skills === undefined)
|
|
59
|
+
return all;
|
|
60
|
+
const allowed = new Set(member.skills.map((n) => n.toLowerCase()));
|
|
61
|
+
return all.filter((s) => allowed.has(s.name.toLowerCase()));
|
|
62
|
+
}
|
|
63
|
+
/** "## Your skills" context block (empty when no skills are visible). */
|
|
64
|
+
function skillsBlock(skills) {
|
|
65
|
+
if (skills.length === 0)
|
|
66
|
+
return "";
|
|
67
|
+
const lines = skills
|
|
68
|
+
.map((s) => `- /${s.name}${s.description ? `: ${s.description}` : ""} — ${s.path}`)
|
|
69
|
+
.join("\n");
|
|
70
|
+
return (`## Your skills\nSkills are reusable instruction packages (a SKILL.md per skill). When the ` +
|
|
71
|
+
`user's request matches one, read its SKILL.md file and follow the instructions in it. The user ` +
|
|
72
|
+
`can also invoke one explicitly by starting a message with /<skill-name>.\n${lines}`);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Explicit skill invocation: a message starting with /<skill-name> is
|
|
76
|
+
* expanded into the skill's SKILL.md instructions (the stored user_message
|
|
77
|
+
* keeps the original "/name args" text). Unknown names pass through as-is.
|
|
78
|
+
*/
|
|
79
|
+
async function expandSkillInvocation(scope, text) {
|
|
80
|
+
const m = /^\/([A-Za-z0-9][\w-]*)[ \t]*([\s\S]*)$/.exec(text);
|
|
81
|
+
if (!m)
|
|
82
|
+
return text;
|
|
83
|
+
const skills = await availableSkills(scope);
|
|
84
|
+
const skill = skills.find((s) => s.name.toLowerCase() === m[1].toLowerCase());
|
|
85
|
+
if (!skill)
|
|
86
|
+
return text;
|
|
87
|
+
const body = await readSkill(skill);
|
|
88
|
+
if (!body.trim())
|
|
89
|
+
return text;
|
|
90
|
+
const args = m[2].trim();
|
|
91
|
+
return (`The user invoked the skill "${skill.name}" (${skill.path}). Follow the skill's instructions:\n\n` +
|
|
92
|
+
`<skill name="${skill.name}">\n${body.trim()}\n</skill>\n\n` +
|
|
93
|
+
`Arguments the user passed to the skill: ${args || "(none)"}`);
|
|
94
|
+
}
|
|
45
95
|
/* ---------- scope context ---------- */
|
|
46
96
|
/** "## Your knowledge" block for a team member's connected OKF bundles. */
|
|
47
97
|
async function teamKnowledgeContext(member) {
|
|
@@ -65,7 +115,8 @@ async function teamKnowledgeContext(member) {
|
|
|
65
115
|
`Never hand-edit index.md or log.md, and never add \`verified\` yourself — verification is the human's ` +
|
|
66
116
|
`click in the UI.\n\n`);
|
|
67
117
|
}
|
|
68
|
-
|
|
118
|
+
/** Base context per scope; scopeContext() appends the shared skills block. */
|
|
119
|
+
async function baseScopeContext(scope, agent) {
|
|
69
120
|
if (scope.kind === "team") {
|
|
70
121
|
const member = await getMember(scope.member).catch(() => null);
|
|
71
122
|
if (!member) {
|
|
@@ -90,7 +141,7 @@ async function scopeContext(scope, agent) {
|
|
|
90
141
|
: "") +
|
|
91
142
|
`\nRun a workflow with:\n` +
|
|
92
143
|
` KRAFTWERK_YES=1 npx kraftwerk run <workflow> "<request>"\n` +
|
|
93
|
-
`The command blocks until the run finishes; artifacts land in output/
|
|
144
|
+
`The command blocks until the run finishes; artifacts land in output/runs/*/ — tell the user the ` +
|
|
94
145
|
`run id and summarize the result. Confirm with the user before starting a long or expensive run ` +
|
|
95
146
|
`unless they clearly asked for it.\n\n`
|
|
96
147
|
: "") +
|
|
@@ -141,7 +192,7 @@ async function scopeContext(scope, agent) {
|
|
|
141
192
|
.map((r) => `- ${r.id} — ${r.workflow ?? "?"} [${r.status}] ${r.request ?? ""}`.trim())
|
|
142
193
|
.join("\n");
|
|
143
194
|
return (`You are the assistant inside the kraftwerk inspector, a UI for a workflow-as-code agent framework. ` +
|
|
144
|
-
`The consumer project root is ${getProjectRoot()}; run outputs live in ${getOutputDir()} (one
|
|
195
|
+
`The consumer project root is ${getProjectRoot()}; run outputs live in ${getOutputDir()} (one folder per run under runs/, each with a trace.jsonl and working files).\n\n` +
|
|
145
196
|
`Workflows in this project:\n${wfLines || "(none)"}\n\nRecent runs:\n${runLines || "(none)"}\n\n` +
|
|
146
197
|
`Answer questions about workflows and runs by reading these files. Do not modify run outputs unless asked.`);
|
|
147
198
|
}
|
|
@@ -160,16 +211,41 @@ async function scopeContext(scope, agent) {
|
|
|
160
211
|
}
|
|
161
212
|
return "";
|
|
162
213
|
}
|
|
214
|
+
async function scopeContext(scope, agent) {
|
|
215
|
+
const [base, skills] = await Promise.all([
|
|
216
|
+
baseScopeContext(scope, agent),
|
|
217
|
+
availableSkills(scope),
|
|
218
|
+
]);
|
|
219
|
+
const block = skillsBlock(skills);
|
|
220
|
+
if (!block)
|
|
221
|
+
return base;
|
|
222
|
+
return base ? `${base}\n\n${block}` : block;
|
|
223
|
+
}
|
|
163
224
|
/* ---------- backend lifecycle ---------- */
|
|
164
225
|
/** Team chats carry the member's model/effort; resolved live so edits apply to new backends. */
|
|
165
|
-
async function backendTuning(scope) {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
226
|
+
async function backendTuning(scope, agent) {
|
|
227
|
+
const tuning = {};
|
|
228
|
+
if (scope.kind === "team") {
|
|
229
|
+
const member = await getMember(scope.member).catch(() => null);
|
|
230
|
+
if (member?.model)
|
|
231
|
+
tuning.model = member.model;
|
|
232
|
+
if (member?.effort)
|
|
233
|
+
tuning.effort = member.effort;
|
|
234
|
+
// Claude discovers skills natively; a defined allowlist narrows that.
|
|
235
|
+
if (agent === "claude" && member?.skills)
|
|
236
|
+
tuning.skills = member.skills;
|
|
237
|
+
}
|
|
238
|
+
// Run chats live in the run folder — grant claude the project root so
|
|
239
|
+
// project-level skills and files stay reachable.
|
|
240
|
+
if (scope.kind === "run" && agent === "claude")
|
|
241
|
+
tuning.addDirs = [getProjectRoot()];
|
|
242
|
+
// pi loads skills only when told: hand it every visible skill folder.
|
|
243
|
+
if (agent === "pi") {
|
|
244
|
+
const skills = await availableSkills(scope);
|
|
245
|
+
if (skills.length > 0)
|
|
246
|
+
tuning.skillDirs = skills.map((s) => s.dir);
|
|
247
|
+
}
|
|
248
|
+
return tuning;
|
|
173
249
|
}
|
|
174
250
|
async function ensureBackend(state) {
|
|
175
251
|
if (state.backend)
|
|
@@ -199,7 +275,7 @@ async function ensureBackend(state) {
|
|
|
199
275
|
},
|
|
200
276
|
};
|
|
201
277
|
const { agent, cwd } = state.meta;
|
|
202
|
-
const tuning = await backendTuning(state.meta.scope);
|
|
278
|
+
const tuning = await backendTuning(state.meta.scope, agent);
|
|
203
279
|
state.backend =
|
|
204
280
|
agent === "pi"
|
|
205
281
|
? startPiBackend(cwd, hooks, tuning)
|
|
@@ -263,7 +339,10 @@ export async function postMessage(id, text) {
|
|
|
263
339
|
try {
|
|
264
340
|
const backend = await ensureBackend(state);
|
|
265
341
|
const context = isFirst ? await scopeContext(state.meta.scope, state.meta.agent) : "";
|
|
266
|
-
|
|
342
|
+
// "/<skill> args" expands to the skill's instructions for the agent;
|
|
343
|
+
// the thread keeps the short form the user typed.
|
|
344
|
+
const body = await expandSkillInvocation(state.meta.scope, text);
|
|
345
|
+
const promptText = context ? `<context>\n${context}\n</context>\n\n${body}` : body;
|
|
267
346
|
const stopReason = await backend.prompt(promptText);
|
|
268
347
|
emit(state, { type: "turn_end", stopReason });
|
|
269
348
|
}
|
|
@@ -291,6 +370,18 @@ export async function resolvePermission(id, requestId, optionId) {
|
|
|
291
370
|
resolve(optionId);
|
|
292
371
|
return {};
|
|
293
372
|
}
|
|
373
|
+
/** Delete a chat: kill its backend, drop the live state, remove its folder. */
|
|
374
|
+
export async function deleteChat(id) {
|
|
375
|
+
const state = await loadState(id);
|
|
376
|
+
if (!state)
|
|
377
|
+
return { error: "not found" };
|
|
378
|
+
for (const resolve of state.pendingPermissions.values())
|
|
379
|
+
resolve(null);
|
|
380
|
+
state.backend?.dispose();
|
|
381
|
+
states.delete(id);
|
|
382
|
+
await fs.rm(safeChatDir(id), { recursive: true, force: true });
|
|
383
|
+
return {};
|
|
384
|
+
}
|
|
294
385
|
export async function cancelChat(id) {
|
|
295
386
|
const state = await loadState(id);
|
|
296
387
|
if (!state)
|