@netnodeag/kraftwerk 0.4.1 → 0.7.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 +192 -6
- package/dist/cli/doctor.js +77 -2
- package/dist/cli/init.js +65 -15
- package/dist/cli/knowledge.d.ts +2 -0
- package/dist/cli/knowledge.js +213 -0
- package/dist/cli/kraftwerk.js +12 -10
- package/dist/cli/runs.js +13 -6
- package/dist/cli/ui.js +3 -4
- package/dist/config.d.ts +9 -0
- package/dist/config.js +5 -2
- package/dist/gates.d.ts +7 -0
- package/dist/gates.js +27 -0
- package/dist/harnesses/claude.js +0 -1
- package/dist/inspector/chat/acp.d.ts +2 -2
- package/dist/inspector/chat/acp.js +44 -3
- package/dist/inspector/chat/backend.d.ts +14 -0
- package/dist/inspector/chat/pi.d.ts +2 -0
- package/dist/inspector/chat/pi.js +106 -0
- package/dist/inspector/chat/sessions.d.ts +33 -0
- package/dist/inspector/chat/sessions.js +407 -0
- package/dist/inspector/chat/store.d.ts +8 -0
- package/dist/inspector/chat/store.js +69 -0
- package/dist/inspector/chat/types.d.ts +7 -0
- package/dist/inspector/context.d.ts +7 -2
- package/dist/inspector/context.js +11 -3
- package/dist/inspector/knowledge.d.ts +24 -0
- package/dist/inspector/knowledge.js +41 -0
- package/dist/inspector/routines.d.ts +56 -0
- package/dist/inspector/routines.js +266 -0
- 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 +311 -1
- package/dist/inspector/skills.d.ts +26 -0
- package/dist/inspector/skills.js +65 -0
- package/dist/inspector/team.d.ts +57 -0
- package/dist/inspector/team.js +121 -0
- package/dist/okf.d.ts +118 -0
- package/dist/okf.js +398 -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 +43 -10
- package/inspector/dist/assets/index-C8AKqc_i.css +1 -0
- package/inspector/dist/assets/index-G3eFm1X4.js +13 -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/schema/workflow.schema.json +166 -31
- 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-BCSWkslF.css +0 -1
- package/inspector/dist/assets/index-D6WuziFQ.js +0 -9
package/README.md
CHANGED
|
@@ -56,6 +56,7 @@ kraftwerk list # table: workflows, steps, agents (with
|
|
|
56
56
|
kraftwerk run tagline "https://..." # run; --yes, --verbose
|
|
57
57
|
kraftwerk run # interactive: pick workflow, type the request
|
|
58
58
|
kraftwerk runs # past runs from output/*/trace.jsonl; runs show <id> for detail
|
|
59
|
+
kraftwerk knowledge # Context & Knowledge: OKF bundles (list/get/put/verify/search/...)
|
|
59
60
|
kraftwerk ui # inspector web UI on http://localhost:1981; --port, --output
|
|
60
61
|
kraftwerk doctor # preflight: harness CLIs, docker, workflows, declared env vars
|
|
61
62
|
kraftwerk validate # all discovered — schema + semantics + files, exit 1 on failure
|
|
@@ -69,8 +70,10 @@ kraftwerk runner ps / stop <run-id> # see / stop running sandbox containers
|
|
|
69
70
|
### Project config — kraftwerk.yml
|
|
70
71
|
|
|
71
72
|
Optional, at the project root (also the root marker for the walk-up); all
|
|
72
|
-
fields optional: `workflows:` (workflows root)
|
|
73
|
-
directory, default `output/`)
|
|
73
|
+
fields optional: `workflows:` (workflows root), `output:` (run-artifact
|
|
74
|
+
directory, default `output/`), `knowledge:` (OKF knowledge-bundle root,
|
|
75
|
+
default `knowledge/`), and `agents:` (team agent-definition root, default
|
|
76
|
+
`agents/`).
|
|
74
77
|
|
|
75
78
|
### Triggering from CI / cron / webhooks
|
|
76
79
|
|
|
@@ -116,6 +119,165 @@ agent (Claude Code, Codex): it prints a self-contained brief — schema
|
|
|
116
119
|
example, gates, harness rules, verify ladder — that the agent follows to
|
|
117
120
|
author the workflow folder and validate/smoke it with this CLI.
|
|
118
121
|
|
|
122
|
+
### Inspector on a server — deploy starter
|
|
123
|
+
|
|
124
|
+
The repo root ships [`deploy-starter/`](../deploy-starter/): copy it
|
|
125
|
+
into your consumer repo as `deploy/` and it builds a small image (this
|
|
126
|
+
package from npm + the claude/codex/pi CLIs) that serves `kraftwerk ui`
|
|
127
|
+
for that repo, bind-mounted at `/work`. `compose.yml` is localhost-only
|
|
128
|
+
(SSH tunnel); `compose.traefik.yml` layers traefik routing + mandatory
|
|
129
|
+
basic-auth on top — the UI has no authentication of its own and its chat
|
|
130
|
+
runs coding agents against the mounted repo. Agent logins made inside
|
|
131
|
+
the container persist in the `agent-home` volume. This is a different
|
|
132
|
+
image from the `kraftwerk-runner` sandbox (`runner/Dockerfile`) used by
|
|
133
|
+
`run --sandbox`.
|
|
134
|
+
|
|
135
|
+
## Team — persistent agents
|
|
136
|
+
|
|
137
|
+
The inspector's "team" screen turns chat agents into persistent teammates.
|
|
138
|
+
A team member is one folder under the project's `agents/` root:
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
agents/max/
|
|
142
|
+
agent.yml # name, emoji, description, harness, model, effort, workflows
|
|
143
|
+
system.md # the member's system prompt (its role)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```yaml
|
|
147
|
+
# agents/max/agent.yml
|
|
148
|
+
name: Max
|
|
149
|
+
emoji: 🛠️
|
|
150
|
+
description: Runs and explains this project's workflows
|
|
151
|
+
harness: claude # claude | codex | pi — which chat backend runs it
|
|
152
|
+
model: sonnet # optional; harness default when omitted
|
|
153
|
+
effort: medium # optional: low | medium | high | xhigh | max
|
|
154
|
+
workflows: [tagline, website-check]
|
|
155
|
+
knowledge: [customer-support] # OKF bundles the member consults & maintains
|
|
156
|
+
skills: [report-html] # optional allowlist; omit = all skills, [] = none
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Sessions with a member are ordinary chats scoped `{ kind: "team", member }`,
|
|
160
|
+
listed per member in a second sidebar. On the first message the member gets
|
|
161
|
+
its role plus its connected workflows and knowledge bundles injected as
|
|
162
|
+
context — including how to run workflows
|
|
163
|
+
(`KRAFTWERK_YES=1 npx kraftwerk run <workflow> "<request>"`) and how to read
|
|
164
|
+
and write knowledge through `kraftwerk knowledge` (writes stamped with the
|
|
165
|
+
member's own actor, `<slug>/<harness>`), so it triggers its own workflows
|
|
166
|
+
when a request matches and keeps its bundles current. Model/effort ride on
|
|
167
|
+
backend-specific channels: the claude adapter takes the model via ACP session
|
|
168
|
+
options and the thinking budget via `MAX_THINKING_TOKENS`; codex gets a
|
|
169
|
+
`CODEX_CONFIG` env override (`model`, `model_reasoning_effort`); pi gets
|
|
170
|
+
`--model`/`--thinking` flags. Members are created and edited in the UI (or by
|
|
171
|
+
editing the files — the definition is read fresh for each new session).
|
|
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
|
+
|
|
196
|
+
### Routines — scheduled prompts
|
|
197
|
+
|
|
198
|
+
A member can have routines: cron-scheduled prompts, like standing orders for
|
|
199
|
+
an employee. Definitions live next to the member in
|
|
200
|
+
`agents/<slug>/routines.yml` (git-tracked); run state (last run, last
|
|
201
|
+
session, errors) lives in `<output>/routines-state.json`.
|
|
202
|
+
|
|
203
|
+
```yaml
|
|
204
|
+
# agents/max/routines.yml
|
|
205
|
+
- id: morning-check
|
|
206
|
+
name: Morning check
|
|
207
|
+
schedule: "0 9 * * 1-5" # 5-field cron (server local time) or @hourly/@daily/@weekly/@monthly
|
|
208
|
+
prompt: |
|
|
209
|
+
Run the website-check workflow for https://example.com and summarize
|
|
210
|
+
anything that regressed since the last run.
|
|
211
|
+
enabled: true
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The inspector server runs the scheduler in-process (no external cron): every
|
|
215
|
+
due routine opens a fresh session for the member, posts the prompt, and the
|
|
216
|
+
run shows up in the sessions sidebar titled "⏰ <name>". Routine sessions run
|
|
217
|
+
unattended, so tool-permission requests are auto-approved (the
|
|
218
|
+
request/approval pair stays in the thread as an audit trail) — treat routine
|
|
219
|
+
prompts with the same trust as `KRAFTWERK_YES=1` workflow runs. Manage
|
|
220
|
+
routines on the member page: create/edit/delete, toggle enabled, "run now",
|
|
221
|
+
and jump to the last run's session. Missed schedules while the server is
|
|
222
|
+
down are skipped, not replayed.
|
|
223
|
+
|
|
224
|
+
## Context & Knowledge — OKF bundles
|
|
225
|
+
|
|
226
|
+
Alongside runs and chats, a project can keep curated knowledge as
|
|
227
|
+
[OKF v0.2](https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md)
|
|
228
|
+
bundles (Open Knowledge Format): directories of markdown files with YAML
|
|
229
|
+
frontmatter under `knowledge/`, one concept per file. Human-readable,
|
|
230
|
+
agent-parseable, diffable in git — and self-describing: frontmatter
|
|
231
|
+
carries provenance (`sources`, `generated`), trust (`verified`), and
|
|
232
|
+
lifecycle (`status`, `stale_after`).
|
|
233
|
+
|
|
234
|
+
```
|
|
235
|
+
knowledge/
|
|
236
|
+
customer-support/ # one bundle per subdirectory
|
|
237
|
+
index.md # derived directory listing (regenerated on every write)
|
|
238
|
+
log.md # chronological update history (appended on every write)
|
|
239
|
+
playbooks/refunds.md # a concept: YAML frontmatter + markdown body
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
The `kraftwerk knowledge` CLI is the enforced write path — `put` stamps
|
|
243
|
+
`generated: { by, at }` with the writing actor, appends the bundle log,
|
|
244
|
+
and regenerates the derived `index.md`, so an agent-maintained corpus
|
|
245
|
+
stays trustable:
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
kraftwerk knowledge init customer-support # new bundle
|
|
249
|
+
kraftwerk knowledge put customer-support/playbooks/refunds \
|
|
250
|
+
--file refunds.md --actor helpdesk-agent/claude-sonnet-5
|
|
251
|
+
kraftwerk knowledge list customer-support # concepts + trust tier
|
|
252
|
+
kraftwerk knowledge get customer-support/playbooks/refunds # raw markdown; --json parsed
|
|
253
|
+
kraftwerk knowledge search "refund" # full-text across bundles
|
|
254
|
+
kraftwerk knowledge verify customer-support/playbooks/refunds --by human:user
|
|
255
|
+
kraftwerk knowledge validate # OKF conformance + warnings
|
|
256
|
+
kraftwerk knowledge fsck --fix # heal out-of-band edits (reindex)
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Actors follow the OKF convention: `<producer>/<version>` for agents,
|
|
260
|
+
`human:<id>` for people, `process:<id>` for automation. Consumers derive
|
|
261
|
+
a **trust tier** per concept: no `verified` ⇒ unverified, verified by
|
|
262
|
+
non-human actors ⇒ machine-confirmed, verified by a `human:` actor ⇒
|
|
263
|
+
human-reviewed. The inspector's **Context & Knowledge** screen shows
|
|
264
|
+
bundles, concepts with trust/status/staleness badges, sources, and the
|
|
265
|
+
bundle log — the ✓ verify button records a `human:user` verification, and
|
|
266
|
+
"curate in chat" opens a knowledge-scoped chat whose agent knows the OKF
|
|
267
|
+
essentials and writes through the CLI.
|
|
268
|
+
|
|
269
|
+
Workflows read and write knowledge through the same CLI via a
|
|
270
|
+
[CLI grant](#cli-grants) — the trust model stays intact because every
|
|
271
|
+
agent write is stamped with its actor:
|
|
272
|
+
|
|
273
|
+
```yaml
|
|
274
|
+
clis:
|
|
275
|
+
npx kraftwerk knowledge: "OKF knowledge base. Read: `list`, `get <bundle>/<path>`, `search <text>`. Write: `put <bundle>/<path> --file <tmp.md> --actor triager/gpt-5.6`. Frontmatter needs `type:`; never edit index.md/log.md by hand."
|
|
276
|
+
agents:
|
|
277
|
+
triager:
|
|
278
|
+
clis: [npx kraftwerk knowledge]
|
|
279
|
+
```
|
|
280
|
+
|
|
119
281
|
## The agent — four axes
|
|
120
282
|
|
|
121
283
|
```ts
|
|
@@ -241,11 +403,35 @@ npm start -- validate <path> # runCli consumers (TS registry)
|
|
|
241
403
|
```
|
|
242
404
|
|
|
243
405
|
Gates: `file_non_empty: <file>`, `slots_filled: <file>`,
|
|
244
|
-
`contains: {file, text, label?}
|
|
406
|
+
`contains: {file, text, label?}`, and `check: <script>` (or
|
|
407
|
+
`check: {run, label?}`) — a bash validation script executed in the run
|
|
408
|
+
directory; exit 0 passes, non-zero fails and everything the script printed
|
|
409
|
+
becomes the failure message, verbatim in the correction prompt. Single-line
|
|
410
|
+
values reference a file inside the workflow folder, like `run:`.
|
|
411
|
+
|
|
412
|
+
Steps take an optional `if:` — deterministic preconditions in the same
|
|
413
|
+
forms as gates, evaluated against the run directory just before the step.
|
|
414
|
+
Any unmet precondition SKIPS the step (traced as `phase_skipped`, run
|
|
415
|
+
continues) — the way to avoid spawning an agent when a previous script
|
|
416
|
+
found nothing to do:
|
|
417
|
+
|
|
418
|
+
```yaml
|
|
419
|
+
- name: triage tickets
|
|
420
|
+
agent: triager
|
|
421
|
+
if:
|
|
422
|
+
- file_non_empty: todo.json # written by the fetch step only when work exists
|
|
423
|
+
prompt: prompts/triage.md
|
|
424
|
+
gates:
|
|
425
|
+
- check: scripts/validate-triage.sh
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Living examples:
|
|
245
429
|
[`../agent-playground/src/workflows/tagline/`](../agent-playground/src/workflows/tagline/)
|
|
246
|
-
and [`../agent-playground/src/workflows/pitch/`](../agent-playground/src/workflows/pitch/)
|
|
247
|
-
|
|
248
|
-
|
|
430
|
+
and [`../agent-playground/src/workflows/pitch/`](../agent-playground/src/workflows/pitch/);
|
|
431
|
+
`check`/`if` in [`../agent-playground/src/workflows/helpdesk-check/`](../agent-playground/src/workflows/helpdesk-check/).
|
|
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.
|
|
249
435
|
|
|
250
436
|
### MCP servers alongside the workflow
|
|
251
437
|
|
package/dist/cli/doctor.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { readFile, stat } from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import chalk from "chalk";
|
|
4
5
|
import { resolveProject } from "../config.js";
|
|
@@ -13,6 +14,31 @@ const ICONS = {
|
|
|
13
14
|
function report(level, label, detail) {
|
|
14
15
|
console.log(`${ICONS[level]} ${label}${detail ? chalk.dim(` — ${detail}`) : ""}`);
|
|
15
16
|
}
|
|
17
|
+
/** True if a < b for plain x.y.z versions (prerelease tags ignored). */
|
|
18
|
+
function semverLt(a, b) {
|
|
19
|
+
const pa = a.split("-")[0].split(".").map(Number);
|
|
20
|
+
const pb = b.split("-")[0].split(".").map(Number);
|
|
21
|
+
for (let i = 0; i < 3; i++) {
|
|
22
|
+
if ((pa[i] ?? 0) !== (pb[i] ?? 0))
|
|
23
|
+
return (pa[i] ?? 0) < (pb[i] ?? 0);
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
/** Latest published version from the npm registry, or undefined if unreachable. */
|
|
28
|
+
async function latestNpmVersion(name) {
|
|
29
|
+
try {
|
|
30
|
+
const res = await fetch(`https://registry.npmjs.org/${name}/latest`, {
|
|
31
|
+
signal: AbortSignal.timeout(5_000),
|
|
32
|
+
});
|
|
33
|
+
if (!res.ok)
|
|
34
|
+
return undefined;
|
|
35
|
+
const data = (await res.json());
|
|
36
|
+
return data.version;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
16
42
|
function cliVersion(command) {
|
|
17
43
|
const r = spawnSync(command, ["--version"], { encoding: "utf8", timeout: 10_000 });
|
|
18
44
|
if (r.status !== 0)
|
|
@@ -29,9 +55,58 @@ export async function runDoctor(cwd) {
|
|
|
29
55
|
report("fail", `node ${process.versions.node}`, "kraftwerk needs node >= 20");
|
|
30
56
|
failures++;
|
|
31
57
|
}
|
|
32
|
-
//
|
|
33
|
-
const
|
|
58
|
+
// kraftwerk version vs npm.
|
|
59
|
+
const pkg = JSON.parse(await readFile(new URL("../../package.json", import.meta.url), "utf8"));
|
|
60
|
+
const latest = await latestNpmVersion(pkg.name);
|
|
61
|
+
if (!latest) {
|
|
62
|
+
report("info", `kraftwerk ${pkg.version}`, "could not reach npm registry to check for updates");
|
|
63
|
+
}
|
|
64
|
+
else if (semverLt(pkg.version, latest)) {
|
|
65
|
+
report("warn", `kraftwerk ${pkg.version}`, `${latest} available — npm i -g ${pkg.name}@latest`);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
report("ok", `kraftwerk ${pkg.version}`, latest === pkg.version ? "latest on npm" : `ahead of npm (latest: ${latest})`);
|
|
69
|
+
}
|
|
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
|
+
}
|
|
34
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
|
+
}
|
|
35
110
|
const found = project.workflowsRoot ? await discoverWorkflows(cwd) : [];
|
|
36
111
|
if (!project.workflowsRoot) {
|
|
37
112
|
report("warn", "no workflows root", "expected src/workflows/ or workflows/ — `kraftwerk init` scaffolds one");
|
package/dist/cli/init.js
CHANGED
|
@@ -2,16 +2,22 @@ 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
|
|
17
|
+
workflows: ${DATA_DIR}/workflows # where workflows live
|
|
18
|
+
output: ${DATA_DIR}/output # where run artifacts land (git-ignored)
|
|
19
|
+
knowledge: ${DATA_DIR}/knowledge # OKF knowledge bundles
|
|
20
|
+
agents: ${DATA_DIR}/agents # team agent definitions
|
|
15
21
|
`;
|
|
16
22
|
const WORKFLOW_TEMPLATE = `# yaml-language-server: $schema=${SCHEMA_URL}
|
|
17
23
|
name: hello
|
|
@@ -38,6 +44,34 @@ const PROMPT_TEMPLATE = `Request: \${{ request }}
|
|
|
38
44
|
Answer the request and write the answer to answer.md
|
|
39
45
|
(Markdown, with a short heading).
|
|
40
46
|
`;
|
|
47
|
+
const AGENT_YML_TEMPLATE = `name: Max - Workflow Runner
|
|
48
|
+
emoji: 🛠️
|
|
49
|
+
description: Runs and explains this project's workflows
|
|
50
|
+
harness: claude
|
|
51
|
+
model: sonnet
|
|
52
|
+
effort: medium
|
|
53
|
+
workflows:
|
|
54
|
+
- hello
|
|
55
|
+
knowledge:
|
|
56
|
+
- demo-customer-support
|
|
57
|
+
`;
|
|
58
|
+
const AGENT_SYSTEM_TEMPLATE = `You are Max, this project's workflow runner. You run and explain the
|
|
59
|
+
project's kraftwerk workflows, and you consult the knowledge bundles you
|
|
60
|
+
have access to before answering questions they cover.
|
|
61
|
+
`;
|
|
62
|
+
const DEMO_BUNDLE = "demo-customer-support";
|
|
63
|
+
const DEMO_CONCEPT = `---
|
|
64
|
+
type: Playbook
|
|
65
|
+
title: Refund handling
|
|
66
|
+
description: How support handles refund requests.
|
|
67
|
+
tags: [ support, refunds ]
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
# Steps
|
|
71
|
+
|
|
72
|
+
1. Check the order in the shop backend.
|
|
73
|
+
2. Refunds under CHF 50 are approved directly.
|
|
74
|
+
`;
|
|
41
75
|
export async function runInit(cwd) {
|
|
42
76
|
const exists = async (p) => !!(await stat(p).catch(() => null));
|
|
43
77
|
const created = [];
|
|
@@ -52,20 +86,36 @@ export async function runInit(cwd) {
|
|
|
52
86
|
await writeFile(abs, content);
|
|
53
87
|
created.push(rel);
|
|
54
88
|
};
|
|
55
|
-
await put("kraftwerk.yml",
|
|
56
|
-
await put(
|
|
57
|
-
await put(
|
|
58
|
-
await put(
|
|
59
|
-
//
|
|
89
|
+
await put("kraftwerk.yml", configTemplate(path.basename(path.resolve(cwd))));
|
|
90
|
+
await put(`${DATA_DIR}/workflows/hello/workflow.yml`, WORKFLOW_TEMPLATE);
|
|
91
|
+
await put(`${DATA_DIR}/workflows/hello/prompts/assistant.md`, PERSONA_TEMPLATE);
|
|
92
|
+
await put(`${DATA_DIR}/workflows/hello/prompts/answer.md`, PROMPT_TEMPLATE);
|
|
93
|
+
// Team agent "max".
|
|
94
|
+
await put(`${DATA_DIR}/agents/max/agent.yml`, AGENT_YML_TEMPLATE);
|
|
95
|
+
await put(`${DATA_DIR}/agents/max/system.md`, AGENT_SYSTEM_TEMPLATE);
|
|
96
|
+
// Demo knowledge bundle, through the canonical OKF write path so
|
|
97
|
+
// index.md and log.md come out consistent.
|
|
98
|
+
const knowledgeRoot = path.join(cwd, DATA_DIR, "knowledge");
|
|
99
|
+
const bundleRel = `${DATA_DIR}/knowledge/${DEMO_BUNDLE}/`;
|
|
100
|
+
if (await exists(path.join(knowledgeRoot, DEMO_BUNDLE))) {
|
|
101
|
+
skipped.push(bundleRel);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
await initBundle(knowledgeRoot, DEMO_BUNDLE);
|
|
105
|
+
await writeConcept(knowledgeRoot, DEMO_BUNDLE, "playbooks/refunds", DEMO_CONCEPT, "kraftwerk-init");
|
|
106
|
+
created.push(bundleRel);
|
|
107
|
+
}
|
|
108
|
+
// .gitignore: append the output dir if it's not covered yet.
|
|
109
|
+
const outputEntry = `${DATA_DIR}/output`;
|
|
60
110
|
const gitignorePath = path.join(cwd, ".gitignore");
|
|
61
111
|
const gitignore = (await readFile(gitignorePath, "utf8").catch(() => null)) ?? null;
|
|
62
112
|
if (gitignore === null) {
|
|
63
|
-
await writeFile(gitignorePath,
|
|
113
|
+
await writeFile(gitignorePath, `${outputEntry}/\n`);
|
|
64
114
|
created.push(".gitignore");
|
|
65
115
|
}
|
|
66
|
-
else if (!gitignore.split("\n").some((l) => l.trim().replace(/\/$/, "") ===
|
|
67
|
-
await appendFile(gitignorePath, `${gitignore.endsWith("\n") ? "" : "\n"}
|
|
68
|
-
created.push(
|
|
116
|
+
else if (!gitignore.split("\n").some((l) => l.trim().replace(/\/$/, "") === outputEntry)) {
|
|
117
|
+
await appendFile(gitignorePath, `${gitignore.endsWith("\n") ? "" : "\n"}${outputEntry}/\n`);
|
|
118
|
+
created.push(`.gitignore (${outputEntry}/ added)`);
|
|
69
119
|
}
|
|
70
120
|
else {
|
|
71
121
|
skipped.push(".gitignore");
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { resolveProject } from "../config.js";
|
|
5
|
+
import { fsck, getConcept, initBundle, knowledgeRoot, listBundles, listConcepts, searchKnowledge, validateBundle, verifyConcept, writeConcept, } from "../okf.js";
|
|
6
|
+
/**
|
|
7
|
+
* `kraftwerk knowledge` — the CLI surface over OKF bundles (Context &
|
|
8
|
+
* Knowledge). This is the enforced write path: `put` stamps provenance
|
|
9
|
+
* (`generated: { by, at }`), appends the bundle log, and regenerates
|
|
10
|
+
* index.md; direct file edits are healed by `fsck --fix`.
|
|
11
|
+
*
|
|
12
|
+
* Agents identify via --actor (or KRAFTWERK_ACTOR); the OKF convention is
|
|
13
|
+
* `<producer>/<version>` for agents, `human:<id>` for people,
|
|
14
|
+
* `process:<id>` for automation. Humans default to `human:user`.
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_ACTOR = "human:user";
|
|
17
|
+
const actorOf = (opts) => opts.actor?.trim() || process.env.KRAFTWERK_ACTOR?.trim() || DEFAULT_ACTOR;
|
|
18
|
+
async function root() {
|
|
19
|
+
const project = await resolveProject(process.cwd());
|
|
20
|
+
return knowledgeRoot(project.root, project.config.knowledge);
|
|
21
|
+
}
|
|
22
|
+
/** Split "bundle/path/to/concept" into its bundle and concept id. */
|
|
23
|
+
function splitRef(ref) {
|
|
24
|
+
const clean = ref.replace(/^\/+/, "");
|
|
25
|
+
const slash = clean.indexOf("/");
|
|
26
|
+
if (slash <= 0 || slash === clean.length - 1) {
|
|
27
|
+
throw new Error(`expected <bundle>/<concept-path>, got "${ref}"`);
|
|
28
|
+
}
|
|
29
|
+
return { bundle: clean.slice(0, slash), id: clean.slice(slash + 1) };
|
|
30
|
+
}
|
|
31
|
+
const tierColor = (tier) => tier === "human-reviewed"
|
|
32
|
+
? chalk.green(tier)
|
|
33
|
+
: tier === "machine-confirmed"
|
|
34
|
+
? chalk.cyan(tier)
|
|
35
|
+
: chalk.dim(tier);
|
|
36
|
+
function conceptLine(c) {
|
|
37
|
+
const flags = [
|
|
38
|
+
c.status !== "stable" ? chalk.yellow(c.status) : "",
|
|
39
|
+
c.stale ? chalk.red("stale") : "",
|
|
40
|
+
c.error ? chalk.red("invalid") : "",
|
|
41
|
+
]
|
|
42
|
+
.filter(Boolean)
|
|
43
|
+
.join(" ");
|
|
44
|
+
return (` ${chalk.cyan(c.id.padEnd(32))} ${(c.type ?? "?").padEnd(22)} ` +
|
|
45
|
+
`${tierColor(c.trustTier).padEnd(18)} ${flags}`);
|
|
46
|
+
}
|
|
47
|
+
function printIssues(issues) {
|
|
48
|
+
for (const i of issues) {
|
|
49
|
+
const mark = i.level === "error" ? chalk.red("✖") : chalk.yellow("⚠");
|
|
50
|
+
console.log(`${mark} ${i.bundle}/${i.file}: ${i.message}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function readStdin() {
|
|
54
|
+
let data = "";
|
|
55
|
+
for await (const chunk of process.stdin)
|
|
56
|
+
data += chunk;
|
|
57
|
+
return data;
|
|
58
|
+
}
|
|
59
|
+
export function registerKnowledgeCommands(program) {
|
|
60
|
+
const knowledge = program
|
|
61
|
+
.command("knowledge")
|
|
62
|
+
.description("Context & Knowledge: OKF bundles under knowledge/ (read, write, verify)");
|
|
63
|
+
knowledge
|
|
64
|
+
.command("list", { isDefault: true })
|
|
65
|
+
.description("List bundles, or the concepts of one bundle")
|
|
66
|
+
.argument("[bundle]", "Bundle name (a directory under knowledge/)")
|
|
67
|
+
.option("--json", "Machine-readable output")
|
|
68
|
+
.action(async (bundle, opts) => {
|
|
69
|
+
const kroot = await root();
|
|
70
|
+
if (!bundle) {
|
|
71
|
+
const bundles = await listBundles(kroot);
|
|
72
|
+
if (opts.json)
|
|
73
|
+
return void console.log(JSON.stringify({ root: kroot, bundles }, null, 2));
|
|
74
|
+
if (bundles.length === 0) {
|
|
75
|
+
console.log(chalk.yellow(`No knowledge bundles in ${kroot} — create one with \`kraftwerk knowledge init <name>\`.`));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
for (const b of bundles) {
|
|
79
|
+
console.log(`${chalk.cyan(b.name.padEnd(24))} ${String(b.concepts).padStart(3)} concepts` +
|
|
80
|
+
chalk.dim(`${b.okfVersion ? ` okf ${b.okfVersion}` : ""}${b.updatedAt ? ` updated ${b.updatedAt.slice(0, 10)}` : ""}`));
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const concepts = await listConcepts(kroot, bundle);
|
|
85
|
+
if (opts.json)
|
|
86
|
+
return void console.log(JSON.stringify({ bundle, concepts }, null, 2));
|
|
87
|
+
if (concepts.length === 0) {
|
|
88
|
+
console.log(chalk.yellow(`Bundle "${bundle}" has no concepts.`));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
console.log(chalk.bold(` ${"concept".padEnd(32)} ${"type".padEnd(22)} trust`));
|
|
92
|
+
for (const c of concepts)
|
|
93
|
+
console.log(conceptLine(c));
|
|
94
|
+
});
|
|
95
|
+
knowledge
|
|
96
|
+
.command("get")
|
|
97
|
+
.description("Print one concept (raw markdown; --json adds parsed frontmatter + trust)")
|
|
98
|
+
.argument("<ref>", "<bundle>/<concept-path>, e.g. finance/metrics/revenue")
|
|
99
|
+
.option("--json", "Machine-readable output")
|
|
100
|
+
.action(async (ref, opts) => {
|
|
101
|
+
const { bundle, id } = splitRef(ref);
|
|
102
|
+
const concept = await getConcept(await root(), bundle, id);
|
|
103
|
+
if (!concept) {
|
|
104
|
+
console.error(chalk.red(`Concept ${ref} not found.`));
|
|
105
|
+
process.exit(2);
|
|
106
|
+
}
|
|
107
|
+
if (opts.json) {
|
|
108
|
+
const { raw, ...rest } = concept;
|
|
109
|
+
console.log(JSON.stringify(rest, null, 2));
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
process.stdout.write(concept.raw);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
knowledge
|
|
116
|
+
.command("put")
|
|
117
|
+
.description("Write a concept from stdin or --file (stamps generated.by/at, logs, reindexes)")
|
|
118
|
+
.argument("<ref>", "<bundle>/<concept-path>")
|
|
119
|
+
.option("--file <path>", "Read the concept markdown from a file instead of stdin")
|
|
120
|
+
.option("--actor <actor>", `Who writes: <producer>/<version>, human:<id>, process:<id> (default: ${DEFAULT_ACTOR})`)
|
|
121
|
+
.action(async (ref, opts) => {
|
|
122
|
+
const { bundle, id } = splitRef(ref);
|
|
123
|
+
const content = opts.file ? await readFile(path.resolve(opts.file), "utf8") : await readStdin();
|
|
124
|
+
if (!content.trim()) {
|
|
125
|
+
console.error(chalk.red("No content (pipe markdown on stdin or pass --file)."));
|
|
126
|
+
process.exit(2);
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const result = await writeConcept(await root(), bundle, id, content, actorOf(opts));
|
|
130
|
+
console.log(`${chalk.green("✔")} ${result.created ? "created" : "updated"} ${chalk.cyan(`${bundle}/${result.concept.id}`)} ` +
|
|
131
|
+
chalk.dim(`(${result.concept.type}, by ${actorOf(opts)})`));
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
console.error(chalk.red(err.message));
|
|
135
|
+
process.exit(2);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
knowledge
|
|
139
|
+
.command("verify")
|
|
140
|
+
.description("Record a verification event on a concept (raises its trust tier)")
|
|
141
|
+
.argument("<ref>", "<bundle>/<concept-path>")
|
|
142
|
+
.option("--by <actor>", `Verifier actor (default: ${DEFAULT_ACTOR})`)
|
|
143
|
+
.action(async (ref, opts) => {
|
|
144
|
+
const { bundle, id } = splitRef(ref);
|
|
145
|
+
const actor = opts.by?.trim() || process.env.KRAFTWERK_ACTOR?.trim() || DEFAULT_ACTOR;
|
|
146
|
+
try {
|
|
147
|
+
const concept = await verifyConcept(await root(), bundle, id, actor);
|
|
148
|
+
console.log(`${chalk.green("✔")} ${ref} verified by ${actor} → ${tierColor(concept.trustTier)}`);
|
|
149
|
+
}
|
|
150
|
+
catch (err) {
|
|
151
|
+
console.error(chalk.red(err.message));
|
|
152
|
+
process.exit(2);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
knowledge
|
|
156
|
+
.command("search")
|
|
157
|
+
.description("Full-text search across bundles")
|
|
158
|
+
.argument("<query>", "Search text (case-insensitive substring)")
|
|
159
|
+
.option("--bundle <name>", "Restrict to one bundle")
|
|
160
|
+
.option("--json", "Machine-readable output")
|
|
161
|
+
.action(async (query, opts) => {
|
|
162
|
+
const hits = await searchKnowledge(await root(), query, opts.bundle);
|
|
163
|
+
if (opts.json)
|
|
164
|
+
return void console.log(JSON.stringify({ query, hits }, null, 2));
|
|
165
|
+
if (hits.length === 0)
|
|
166
|
+
return void console.log(chalk.dim("No matches."));
|
|
167
|
+
for (const h of hits) {
|
|
168
|
+
console.log(`${chalk.cyan(`${h.bundle}/${h.id}`)} ${chalk.dim(`(${h.type ?? "?"})`)}\n ${h.snippet}`);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
knowledge
|
|
172
|
+
.command("init")
|
|
173
|
+
.description("Create an empty bundle (index.md + log.md)")
|
|
174
|
+
.argument("<bundle>", "Bundle name")
|
|
175
|
+
.action(async (bundle) => {
|
|
176
|
+
try {
|
|
177
|
+
const dir = await initBundle(await root(), bundle);
|
|
178
|
+
console.log(`${chalk.green("✔")} bundle ${chalk.cyan(bundle)} created at ${dir}`);
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
console.error(chalk.red(err.message));
|
|
182
|
+
process.exit(2);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
knowledge
|
|
186
|
+
.command("validate")
|
|
187
|
+
.description("OKF conformance check (errors) + provenance/staleness warnings")
|
|
188
|
+
.argument("[bundle]", "Bundle name; without it: all bundles")
|
|
189
|
+
.action(async (bundle) => {
|
|
190
|
+
const kroot = await root();
|
|
191
|
+
const bundles = bundle ? [bundle] : (await listBundles(kroot)).map((b) => b.name);
|
|
192
|
+
let errors = 0;
|
|
193
|
+
for (const b of bundles) {
|
|
194
|
+
const issues = await validateBundle(kroot, b);
|
|
195
|
+
printIssues(issues);
|
|
196
|
+
errors += issues.filter((i) => i.level === "error").length;
|
|
197
|
+
}
|
|
198
|
+
if (errors === 0)
|
|
199
|
+
console.log(chalk.green(`✔ ${bundles.length} bundle(s) conformant`));
|
|
200
|
+
process.exit(errors > 0 ? 1 : 0);
|
|
201
|
+
});
|
|
202
|
+
knowledge
|
|
203
|
+
.command("fsck")
|
|
204
|
+
.description("Validate all bundles; --fix regenerates every derived index.md")
|
|
205
|
+
.option("--fix", "Regenerate index.md files (heals out-of-band writes)")
|
|
206
|
+
.action(async (opts) => {
|
|
207
|
+
const issues = await fsck(await root(), !!opts.fix);
|
|
208
|
+
printIssues(issues);
|
|
209
|
+
if (opts.fix)
|
|
210
|
+
console.log(chalk.green("✔ indexes regenerated"));
|
|
211
|
+
process.exit(issues.some((i) => i.level === "error") ? 1 : 0);
|
|
212
|
+
});
|
|
213
|
+
}
|