@netnodeag/kraftwerk 0.4.0 → 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 +166 -5
- package/dist/cli/doctor.js +38 -0
- package/dist/cli/knowledge.d.ts +2 -0
- package/dist/cli/knowledge.js +213 -0
- package/dist/cli/kraftwerk.js +5 -2
- package/dist/cli/ui.js +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.js +4 -2
- package/dist/gates.d.ts +7 -0
- package/dist/gates.js +27 -0
- package/dist/inspector/chat/acp.d.ts +2 -0
- package/dist/inspector/chat/acp.js +155 -0
- package/dist/inspector/chat/backend.d.ts +36 -0
- package/dist/inspector/chat/backend.js +1 -0
- package/dist/inspector/chat/pi.d.ts +2 -0
- package/dist/inspector/chat/pi.js +105 -0
- package/dist/inspector/chat/sessions.d.ts +29 -0
- package/dist/inspector/chat/sessions.js +316 -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 +77 -0
- package/dist/inspector/chat/types.js +8 -0
- 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/server.js +272 -0
- package/dist/inspector/team.d.ts +50 -0
- package/dist/inspector/team.js +119 -0
- package/dist/okf.d.ts +118 -0
- package/dist/okf.js +398 -0
- package/dist/yaml.js +41 -8
- package/inspector/dist/assets/{index-BCSWkslF.css → index-BtcPa8nx.css} +1 -1
- package/inspector/dist/assets/index-CImcmBVs.js +10 -0
- package/inspector/dist/index.html +2 -2
- package/package.json +4 -1
- package/schema/workflow.schema.json +166 -31
- package/inspector/dist/assets/index-D6WuziFQ.js +0 -9
package/README.md
CHANGED
|
@@ -56,7 +56,8 @@ 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
|
|
59
|
+
kraftwerk knowledge # Context & Knowledge: OKF bundles (list/get/put/verify/search/...)
|
|
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
|
|
62
63
|
kraftwerk validate src/workflows/pitch # specific paths
|
|
@@ -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,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?}
|
|
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
|
|
package/dist/cli/doctor.js
CHANGED
|
@@ -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,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
|
+
}
|
package/dist/cli/kraftwerk.js
CHANGED
|
@@ -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,7 +21,8 @@ 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
|
|
23
|
-
* kraftwerk
|
|
24
|
+
* kraftwerk knowledge ... Context & Knowledge: OKF bundles (list/get/put/verify/...)
|
|
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
|
|
26
28
|
*
|
|
@@ -251,10 +253,11 @@ 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")
|
|
257
|
-
.option("--port <port>", "Port for the web UI", "
|
|
260
|
+
.option("--port <port>", "Port for the web UI", "1981")
|
|
258
261
|
.option("--output <dir>", "Output directory to inspect (default: the project's output dir)")
|
|
259
262
|
.action(async (opts) => {
|
|
260
263
|
await runUi(process.cwd(), opts);
|
package/dist/cli/ui.js
CHANGED
|
@@ -45,7 +45,7 @@ export async function runUi(cwd, opts) {
|
|
|
45
45
|
const outputDir = opts.output
|
|
46
46
|
? path.resolve(cwd, opts.output)
|
|
47
47
|
: (await resolveProject(cwd)).outputDir;
|
|
48
|
-
const port = Number(opts.port ?? "
|
|
48
|
+
const port = Number(opts.port ?? "1981");
|
|
49
49
|
await startInspector({ outputDir, staticDir, port });
|
|
50
50
|
console.log(`${chalk.green("✔")} Inspector: ${chalk.cyan(`http://localhost:${port}`)} ` +
|
|
51
51
|
chalk.dim(`(output: ${outputDir})`));
|
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})`,
|