@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.
Files changed (81) hide show
  1. package/README.md +192 -6
  2. package/dist/cli/doctor.js +77 -2
  3. package/dist/cli/init.js +65 -15
  4. package/dist/cli/knowledge.d.ts +2 -0
  5. package/dist/cli/knowledge.js +213 -0
  6. package/dist/cli/kraftwerk.js +12 -10
  7. package/dist/cli/runs.js +13 -6
  8. package/dist/cli/ui.js +3 -4
  9. package/dist/config.d.ts +9 -0
  10. package/dist/config.js +5 -2
  11. package/dist/gates.d.ts +7 -0
  12. package/dist/gates.js +27 -0
  13. package/dist/harnesses/claude.js +0 -1
  14. package/dist/inspector/chat/acp.d.ts +2 -2
  15. package/dist/inspector/chat/acp.js +44 -3
  16. package/dist/inspector/chat/backend.d.ts +14 -0
  17. package/dist/inspector/chat/pi.d.ts +2 -0
  18. package/dist/inspector/chat/pi.js +106 -0
  19. package/dist/inspector/chat/sessions.d.ts +33 -0
  20. package/dist/inspector/chat/sessions.js +407 -0
  21. package/dist/inspector/chat/store.d.ts +8 -0
  22. package/dist/inspector/chat/store.js +69 -0
  23. package/dist/inspector/chat/types.d.ts +7 -0
  24. package/dist/inspector/context.d.ts +7 -2
  25. package/dist/inspector/context.js +11 -3
  26. package/dist/inspector/knowledge.d.ts +24 -0
  27. package/dist/inspector/knowledge.js +41 -0
  28. package/dist/inspector/routines.d.ts +56 -0
  29. package/dist/inspector/routines.js +266 -0
  30. package/dist/inspector/runner.d.ts +10 -0
  31. package/dist/inspector/runner.js +6 -8
  32. package/dist/inspector/runs.d.ts +2 -0
  33. package/dist/inspector/runs.js +17 -12
  34. package/dist/inspector/server.d.ts +2 -0
  35. package/dist/inspector/server.js +311 -1
  36. package/dist/inspector/skills.d.ts +26 -0
  37. package/dist/inspector/skills.js +65 -0
  38. package/dist/inspector/team.d.ts +57 -0
  39. package/dist/inspector/team.js +121 -0
  40. package/dist/okf.d.ts +118 -0
  41. package/dist/okf.js +398 -0
  42. package/dist/runner/docker.d.ts +3 -1
  43. package/dist/runner/docker.js +5 -5
  44. package/dist/workflow.d.ts +7 -0
  45. package/dist/workflow.js +19 -0
  46. package/dist/yaml.js +43 -10
  47. package/inspector/dist/assets/index-C8AKqc_i.css +1 -0
  48. package/inspector/dist/assets/index-G3eFm1X4.js +13 -0
  49. package/inspector/dist/assets/roboto-latin-400-normal-BqEyEoaF.woff2 +0 -0
  50. package/inspector/dist/assets/roboto-latin-400-normal-D27F5sTu.woff +0 -0
  51. package/inspector/dist/assets/roboto-latin-500-normal-7RbcRiD8.woff2 +0 -0
  52. package/inspector/dist/assets/roboto-latin-500-normal-fsTFPL3E.woff +0 -0
  53. package/inspector/dist/assets/roboto-latin-700-normal-BZpUvMxY.woff2 +0 -0
  54. package/inspector/dist/assets/roboto-latin-700-normal-YuyVweIx.woff +0 -0
  55. package/inspector/dist/assets/roboto-mono-latin-400-normal-DBZPkcnn.woff +0 -0
  56. package/inspector/dist/assets/roboto-mono-latin-400-normal-GekRknry.woff2 +0 -0
  57. package/inspector/dist/assets/roboto-mono-latin-500-normal-D6j6AMzd.woff +0 -0
  58. package/inspector/dist/assets/roboto-mono-latin-500-normal-pxIOXZmA.woff2 +0 -0
  59. package/inspector/dist/assets/roboto-mono-latin-700-normal-CHYrj1DZ.woff +0 -0
  60. package/inspector/dist/assets/roboto-mono-latin-700-normal-foxDnrFi.woff2 +0 -0
  61. package/inspector/dist/index.html +2 -2
  62. package/package.json +1 -1
  63. package/schema/workflow.schema.json +166 -31
  64. package/inspector/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
  65. package/inspector/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
  66. package/inspector/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
  67. package/inspector/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
  68. package/inspector/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
  69. package/inspector/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
  70. package/inspector/dist/assets/ibm-plex-mono-latin-700-normal-7sUh57Bg.woff2 +0 -0
  71. package/inspector/dist/assets/ibm-plex-mono-latin-700-normal-CNHXzs6v.woff +0 -0
  72. package/inspector/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
  73. package/inspector/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
  74. package/inspector/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
  75. package/inspector/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
  76. package/inspector/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
  77. package/inspector/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
  78. package/inspector/dist/assets/ibm-plex-sans-latin-700-normal-Bth3BMcD.woff +0 -0
  79. package/inspector/dist/assets/ibm-plex-sans-latin-700-normal-Bxkt5Cjx.woff2 +0 -0
  80. package/inspector/dist/assets/index-BCSWkslF.css +0 -1
  81. 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) and `output:` (run-artifact
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?}`. Living examples:
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
- v1 is deliberately linear — approval loops, AGENTS.md-style context files
248
- and skills stay on the roadmap; anything non-linear is a TS workflow.
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
 
@@ -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
- // Project.
33
- const project = await resolveProject(cwd);
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), workflows/ with a runnable
8
- * example, and an output/ ignore entry. Everything is idempotent: existing
9
- * files are left untouched and reported.
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 CONFIG_TEMPLATE = `# kraftwerk project config — also marks the project root for the CLI.
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
- workflows: workflows # where workflows live
14
- output: output # where run artifacts land (git-ignored)
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", CONFIG_TEMPLATE);
56
- await put("workflows/hello/workflow.yml", WORKFLOW_TEMPLATE);
57
- await put("workflows/hello/prompts/assistant.md", PERSONA_TEMPLATE);
58
- await put("workflows/hello/prompts/answer.md", PROMPT_TEMPLATE);
59
- // .gitignore: append output/ if it's not covered yet.
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, "output/\n");
113
+ await writeFile(gitignorePath, `${outputEntry}/\n`);
64
114
  created.push(".gitignore");
65
115
  }
66
- else if (!gitignore.split("\n").some((l) => l.trim().replace(/\/$/, "") === "output")) {
67
- await appendFile(gitignorePath, `${gitignore.endsWith("\n") ? "" : "\n"}output/\n`);
68
- created.push(".gitignore (output/ added)");
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,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerKnowledgeCommands(program: Command): void;
@@ -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
+ }