@netnodeag/kraftwerk 0.4.1 → 0.6.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 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,141 @@ 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
+ ```
157
+
158
+ Sessions with a member are ordinary chats scoped `{ kind: "team", member }`,
159
+ listed per member in a second sidebar. On the first message the member gets
160
+ its role plus its connected workflows and knowledge bundles injected as
161
+ context — including how to run workflows
162
+ (`KRAFTWERK_YES=1 npx kraftwerk run <workflow> "<request>"`) and how to read
163
+ and write knowledge through `kraftwerk knowledge` (writes stamped with the
164
+ member's own actor, `<slug>/<harness>`), so it triggers its own workflows
165
+ when a request matches and keeps its bundles current. Model/effort ride on
166
+ backend-specific channels: the claude adapter takes the model via ACP session
167
+ options and the thinking budget via `MAX_THINKING_TOKENS`; codex gets a
168
+ `CODEX_CONFIG` env override (`model`, `model_reasoning_effort`); pi gets
169
+ `--model`/`--thinking` flags. Members are created and edited in the UI (or by
170
+ editing the files — the definition is read fresh for each new session).
171
+
172
+ ### Routines — scheduled prompts
173
+
174
+ A member can have routines: cron-scheduled prompts, like standing orders for
175
+ an employee. Definitions live next to the member in
176
+ `agents/<slug>/routines.yml` (git-tracked); run state (last run, last
177
+ session, errors) lives in `<output>/routines-state.json`.
178
+
179
+ ```yaml
180
+ # agents/max/routines.yml
181
+ - id: morning-check
182
+ name: Morning check
183
+ schedule: "0 9 * * 1-5" # 5-field cron (server local time) or @hourly/@daily/@weekly/@monthly
184
+ prompt: |
185
+ Run the website-check workflow for https://example.com and summarize
186
+ anything that regressed since the last run.
187
+ enabled: true
188
+ ```
189
+
190
+ The inspector server runs the scheduler in-process (no external cron): every
191
+ due routine opens a fresh session for the member, posts the prompt, and the
192
+ run shows up in the sessions sidebar titled "⏰ <name>". Routine sessions run
193
+ unattended, so tool-permission requests are auto-approved (the
194
+ request/approval pair stays in the thread as an audit trail) — treat routine
195
+ prompts with the same trust as `KRAFTWERK_YES=1` workflow runs. Manage
196
+ routines on the member page: create/edit/delete, toggle enabled, "run now",
197
+ and jump to the last run's session. Missed schedules while the server is
198
+ down are skipped, not replayed.
199
+
200
+ ## Context & Knowledge — OKF bundles
201
+
202
+ Alongside runs and chats, a project can keep curated knowledge as
203
+ [OKF v0.2](https://github.com/GoogleCloudPlatform/knowledge-catalog/blob/main/okf/SPEC.md)
204
+ bundles (Open Knowledge Format): directories of markdown files with YAML
205
+ frontmatter under `knowledge/`, one concept per file. Human-readable,
206
+ agent-parseable, diffable in git — and self-describing: frontmatter
207
+ carries provenance (`sources`, `generated`), trust (`verified`), and
208
+ lifecycle (`status`, `stale_after`).
209
+
210
+ ```
211
+ knowledge/
212
+ customer-support/ # one bundle per subdirectory
213
+ index.md # derived directory listing (regenerated on every write)
214
+ log.md # chronological update history (appended on every write)
215
+ playbooks/refunds.md # a concept: YAML frontmatter + markdown body
216
+ ```
217
+
218
+ The `kraftwerk knowledge` CLI is the enforced write path — `put` stamps
219
+ `generated: { by, at }` with the writing actor, appends the bundle log,
220
+ and regenerates the derived `index.md`, so an agent-maintained corpus
221
+ stays trustable:
222
+
223
+ ```bash
224
+ kraftwerk knowledge init customer-support # new bundle
225
+ kraftwerk knowledge put customer-support/playbooks/refunds \
226
+ --file refunds.md --actor helpdesk-agent/claude-sonnet-5
227
+ kraftwerk knowledge list customer-support # concepts + trust tier
228
+ kraftwerk knowledge get customer-support/playbooks/refunds # raw markdown; --json parsed
229
+ kraftwerk knowledge search "refund" # full-text across bundles
230
+ kraftwerk knowledge verify customer-support/playbooks/refunds --by human:user
231
+ kraftwerk knowledge validate # OKF conformance + warnings
232
+ kraftwerk knowledge fsck --fix # heal out-of-band edits (reindex)
233
+ ```
234
+
235
+ Actors follow the OKF convention: `<producer>/<version>` for agents,
236
+ `human:<id>` for people, `process:<id>` for automation. Consumers derive
237
+ a **trust tier** per concept: no `verified` ⇒ unverified, verified by
238
+ non-human actors ⇒ machine-confirmed, verified by a `human:` actor ⇒
239
+ human-reviewed. The inspector's **Context & Knowledge** screen shows
240
+ bundles, concepts with trust/status/staleness badges, sources, and the
241
+ bundle log — the ✓ verify button records a `human:user` verification, and
242
+ "curate in chat" opens a knowledge-scoped chat whose agent knows the OKF
243
+ essentials and writes through the CLI.
244
+
245
+ Workflows read and write knowledge through the same CLI via a
246
+ [CLI grant](#cli-grants) — the trust model stays intact because every
247
+ agent write is stamped with its actor:
248
+
249
+ ```yaml
250
+ clis:
251
+ 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."
252
+ agents:
253
+ triager:
254
+ clis: [npx kraftwerk knowledge]
255
+ ```
256
+
119
257
  ## The agent — four axes
120
258
 
121
259
  ```ts
@@ -241,9 +379,32 @@ npm start -- validate <path> # runCli consumers (TS registry)
241
379
  ```
242
380
 
243
381
  Gates: `file_non_empty: <file>`, `slots_filled: <file>`,
244
- `contains: {file, text, label?}`. Living examples:
382
+ `contains: {file, text, label?}`, and `check: <script>` (or
383
+ `check: {run, label?}`) — a bash validation script executed in the run
384
+ directory; exit 0 passes, non-zero fails and everything the script printed
385
+ becomes the failure message, verbatim in the correction prompt. Single-line
386
+ values reference a file inside the workflow folder, like `run:`.
387
+
388
+ Steps take an optional `if:` — deterministic preconditions in the same
389
+ forms as gates, evaluated against the run directory just before the step.
390
+ Any unmet precondition SKIPS the step (traced as `phase_skipped`, run
391
+ continues) — the way to avoid spawning an agent when a previous script
392
+ found nothing to do:
393
+
394
+ ```yaml
395
+ - name: triage tickets
396
+ agent: triager
397
+ if:
398
+ - file_non_empty: todo.json # written by the fetch step only when work exists
399
+ prompt: prompts/triage.md
400
+ gates:
401
+ - check: scripts/validate-triage.sh
402
+ ```
403
+
404
+ Living examples:
245
405
  [`../agent-playground/src/workflows/tagline/`](../agent-playground/src/workflows/tagline/)
246
- and [`../agent-playground/src/workflows/pitch/`](../agent-playground/src/workflows/pitch/).
406
+ and [`../agent-playground/src/workflows/pitch/`](../agent-playground/src/workflows/pitch/);
407
+ `check`/`if` in [`../agent-playground/src/workflows/helpdesk-check/`](../agent-playground/src/workflows/helpdesk-check/).
247
408
  v1 is deliberately linear — approval loops, AGENTS.md-style context files
248
409
  and skills stay on the roadmap; anything non-linear is a TS workflow.
249
410
 
@@ -1,4 +1,5 @@
1
1
  import { spawnSync } from "node:child_process";
2
+ import { readFile } 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,6 +55,18 @@ export async function runDoctor(cwd) {
29
55
  report("fail", `node ${process.versions.node}`, "kraftwerk needs node >= 20");
30
56
  failures++;
31
57
  }
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
+ }
32
70
  // Project.
33
71
  const project = await resolveProject(cwd);
34
72
  report("info", `Project root: ${project.root}`, project.configPath ? path.basename(project.configPath) : "no kraftwerk.yml (fallback: workflows folder or .git)");
@@ -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
+ }
@@ -11,6 +11,7 @@ import { loadWorkflow, missingEnv } from "../yaml.js";
11
11
  import { renderCreateBrief } from "./create-brief.js";
12
12
  import { runDoctor } from "./doctor.js";
13
13
  import { runInit } from "./init.js";
14
+ import { registerKnowledgeCommands } from "./knowledge.js";
14
15
  import { listRuns, showRun } from "./runs.js";
15
16
  import { runUi } from "./ui.js";
16
17
  /**
@@ -20,6 +21,7 @@ import { runUi } from "./ui.js";
20
21
  * kraftwerk list discover + list workflows (--json, --from)
21
22
  * kraftwerk run [workflow] [text] run one (prompts interactively if omitted)
22
23
  * kraftwerk runs [show <id>] inspect past runs from their traces
24
+ * kraftwerk knowledge ... Context & Knowledge: OKF bundles (list/get/put/verify/...)
23
25
  * kraftwerk ui start the inspector web UI (localhost:1981)
24
26
  * kraftwerk doctor preflight: harness CLIs, docker, workflows, env
25
27
  * kraftwerk validate [paths...] validate without executing
@@ -251,6 +253,7 @@ runs
251
253
  .action(async (runId, opts) => {
252
254
  await showRun(process.cwd(), runId, opts);
253
255
  });
256
+ registerKnowledgeCommands(program);
254
257
  program
255
258
  .command("ui")
256
259
  .description("Start the inspector web UI for this project's runs and workflows")
package/dist/config.d.ts CHANGED
@@ -11,6 +11,8 @@
11
11
  *
12
12
  * workflows: src/workflows # workflows root, relative to the file
13
13
  * output: output # run-artifact directory, relative to the file
14
+ * knowledge: knowledge # OKF knowledge-bundle root, relative to the file
15
+ * agents: agents # team agent-definition root, relative to the file
14
16
  */
15
17
  /** Stable, versionless URL of the workflow JSON schema (editor validation). */
16
18
  export declare const SCHEMA_URL = "https://raw.githubusercontent.com/NETNODEAG/kraftwerk/main/kraftwerk/schema/workflow.schema.json";
@@ -20,6 +22,10 @@ export interface ProjectConfig {
20
22
  workflows?: string;
21
23
  /** Run-artifact directory relative to the project root. Default: output */
22
24
  output?: string;
25
+ /** OKF knowledge-bundle root relative to the project root. Default: knowledge */
26
+ knowledge?: string;
27
+ /** Team agent-definition root relative to the project root. Default: agents */
28
+ agents?: string;
23
29
  }
24
30
  export interface Project {
25
31
  /** Absolute project root the CLI operates on. */
package/dist/config.js CHANGED
@@ -14,6 +14,8 @@ import { parse } from "yaml";
14
14
  *
15
15
  * workflows: src/workflows # workflows root, relative to the file
16
16
  * output: output # run-artifact directory, relative to the file
17
+ * knowledge: knowledge # OKF knowledge-bundle root, relative to the file
18
+ * agents: agents # team agent-definition root, relative to the file
17
19
  */
18
20
  /** Stable, versionless URL of the workflow JSON schema (editor validation). */
19
21
  export const SCHEMA_URL = "https://raw.githubusercontent.com/NETNODEAG/kraftwerk/main/kraftwerk/schema/workflow.schema.json";
@@ -81,10 +83,10 @@ async function loadConfig(configPath) {
81
83
  if (raw === null || raw === undefined)
82
84
  return {};
83
85
  if (typeof raw !== "object" || Array.isArray(raw)) {
84
- throw new Error(`${path.basename(configPath)}: expected a mapping (workflows, output)`);
86
+ throw new Error(`${path.basename(configPath)}: expected a mapping (workflows, output, knowledge)`);
85
87
  }
86
88
  const config = raw;
87
- const known = ["workflows", "output"];
89
+ const known = ["workflows", "output", "knowledge", "agents"];
88
90
  for (const key of Object.keys(config)) {
89
91
  if (!known.includes(key)) {
90
92
  throw new Error(`${path.basename(configPath)}: unknown key "${key}" (allowed: ${known.join(", ")})`);
package/dist/gates.d.ts CHANGED
@@ -14,5 +14,12 @@ export interface Gate {
14
14
  export declare const fileNonEmpty: (file: string) => Gate;
15
15
  /** No {{...}} template slots may survive a fill step. */
16
16
  export declare const slotsFilled: (file: string) => Gate;
17
+ /**
18
+ * Workflow-specific validation as a bash script: exit 0 = passed, non-zero =
19
+ * failed and everything the script printed becomes the failure message — so
20
+ * precise per-violation output turns directly into a precise correction
21
+ * prompt. The script runs in the run directory (env: RUN_DIR).
22
+ */
23
+ export declare const checkScript: (script: string, label: string) => Gate;
17
24
  /** Fixed template parts chosen by code must survive byte-identically. */
18
25
  export declare const containsText: (file: string, needle: string, label: string) => Gate;
package/dist/gates.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { spawn } from "node:child_process";
1
2
  import { readFile, stat } from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  export const fileNonEmpty = (file) => ({
@@ -22,6 +23,32 @@ export const slotsFilled = (file) => ({
22
23
  : null;
23
24
  },
24
25
  });
26
+ /**
27
+ * Workflow-specific validation as a bash script: exit 0 = passed, non-zero =
28
+ * failed and everything the script printed becomes the failure message — so
29
+ * precise per-violation output turns directly into a precise correction
30
+ * prompt. The script runs in the run directory (env: RUN_DIR).
31
+ */
32
+ export const checkScript = (script, label) => ({
33
+ name: `check(${label})`,
34
+ async check(runDir) {
35
+ const result = await new Promise((resolve, reject) => {
36
+ const child = spawn("bash", ["-c", script], {
37
+ cwd: runDir,
38
+ env: { ...process.env, RUN_DIR: runDir },
39
+ });
40
+ let output = "";
41
+ child.stdout.on("data", (chunk) => (output += String(chunk)));
42
+ child.stderr.on("data", (chunk) => (output += String(chunk)));
43
+ child.on("error", reject);
44
+ child.on("close", (code) => resolve({ code: code ?? 1, output }));
45
+ });
46
+ if (result.code === 0)
47
+ return null;
48
+ const tail = result.output.trim().split("\n").slice(-12).join("\n");
49
+ return tail || `check script failed with exit code ${result.code}`;
50
+ },
51
+ });
25
52
  /** Fixed template parts chosen by code must survive byte-identically. */
26
53
  export const containsText = (file, needle, label) => ({
27
54
  name: `contains(${file}, ${label})`,
@@ -1,2 +1,2 @@
1
- import type { BackendHooks, ChatBackend } from "./backend.js";
2
- export declare function startAcpBackend(agent: "claude" | "codex", cwd: string, hooks: BackendHooks): Promise<ChatBackend>;
1
+ import type { BackendHooks, BackendTuning, ChatBackend } from "./backend.js";
2
+ export declare function startAcpBackend(agent: "claude" | "codex", cwd: string, hooks: BackendHooks, tuning?: BackendTuning): Promise<ChatBackend>;
@@ -15,15 +15,42 @@ const ADAPTERS = {
15
15
  claude: "@agentclientprotocol/claude-agent-acp/dist/index.js",
16
16
  codex: "@agentclientprotocol/codex-acp/dist/index.js",
17
17
  };
18
+ /** Effort tier -> Claude thinking budget (the adapter reads MAX_THINKING_TOKENS). */
19
+ const CLAUDE_THINKING_BUDGET = {
20
+ low: 2048,
21
+ medium: 8192,
22
+ high: 16384,
23
+ xhigh: 32000,
24
+ max: 63999,
25
+ };
26
+ /**
27
+ * Model/effort overrides ride on adapter-specific channels: the claude
28
+ * adapter takes the model via session `_meta.claudeCode.options` and the
29
+ * thinking budget via env; codex-acp merges a CODEX_CONFIG env JSON into
30
+ * the session config it hands to `codex app-server`.
31
+ */
32
+ function tuningEnv(agent, tuning) {
33
+ const env = { ...process.env };
34
+ if (agent === "claude" && tuning.effort && CLAUDE_THINKING_BUDGET[tuning.effort]) {
35
+ env.MAX_THINKING_TOKENS = String(CLAUDE_THINKING_BUDGET[tuning.effort]);
36
+ }
37
+ if (agent === "codex" && (tuning.model || tuning.effort)) {
38
+ env.CODEX_CONFIG = JSON.stringify({
39
+ ...(tuning.model ? { model: tuning.model } : {}),
40
+ ...(tuning.effort ? { model_reasoning_effort: tuning.effort } : {}),
41
+ });
42
+ }
43
+ return env;
44
+ }
18
45
  function contentText(content) {
19
46
  return content.type === "text" ? content.text : "";
20
47
  }
21
- export async function startAcpBackend(agent, cwd, hooks) {
48
+ export async function startAcpBackend(agent, cwd, hooks, tuning = {}) {
22
49
  const entry = fileURLToPath(import.meta.resolve(ADAPTERS[agent]));
23
50
  const child = spawn(process.execPath, [entry], {
24
51
  cwd,
25
52
  stdio: ["pipe", "pipe", "pipe"],
26
- env: process.env,
53
+ env: tuningEnv(agent, tuning),
27
54
  });
28
55
  let stderr = "";
29
56
  child.stderr.on("data", (c) => {
@@ -91,7 +118,13 @@ export async function startAcpBackend(agent, cwd, hooks) {
91
118
  clientCapabilities: { fs: { readTextFile: false, writeTextFile: false } },
92
119
  clientInfo: { name: "kraftwerk-inspector", version: "1.0.0" },
93
120
  });
94
- const session = await conn.newSession({ cwd, mcpServers: [] });
121
+ const session = await conn.newSession({
122
+ cwd,
123
+ mcpServers: [],
124
+ ...(agent === "claude" && tuning.model
125
+ ? { _meta: { claudeCode: { options: { model: tuning.model } } } }
126
+ : {}),
127
+ });
95
128
  const sessionId = session.sessionId;
96
129
  return {
97
130
  async prompt(text) {