@netnodeag/kraftwerk 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -2
- package/dist/cli/doctor.js +40 -3
- package/dist/cli/init.js +66 -15
- package/dist/cli/kraftwerk.js +9 -10
- package/dist/cli/runs.js +13 -6
- package/dist/cli/ui.js +3 -4
- package/dist/config.d.ts +6 -0
- package/dist/config.js +4 -2
- package/dist/harnesses/claude.js +0 -1
- package/dist/inspector/chat/acp.js +10 -2
- package/dist/inspector/chat/backend.d.ts +6 -0
- package/dist/inspector/chat/pi.js +1 -0
- package/dist/inspector/chat/sessions.d.ts +4 -0
- package/dist/inspector/chat/sessions.js +105 -14
- package/dist/inspector/context.d.ts +7 -2
- package/dist/inspector/context.js +11 -3
- package/dist/inspector/runner.d.ts +10 -0
- package/dist/inspector/runner.js +6 -8
- package/dist/inspector/runs.d.ts +2 -0
- package/dist/inspector/runs.js +17 -12
- package/dist/inspector/server.d.ts +2 -0
- package/dist/inspector/server.js +40 -2
- package/dist/inspector/skills.d.ts +26 -0
- package/dist/inspector/skills.js +65 -0
- package/dist/inspector/team.d.ts +7 -0
- package/dist/inspector/team.js +2 -0
- package/dist/runner/docker.d.ts +3 -1
- package/dist/runner/docker.js +5 -5
- package/dist/workflow.d.ts +7 -0
- package/dist/workflow.js +19 -0
- package/dist/yaml.js +2 -2
- package/inspector/dist/assets/index-Bs8rMhqu.js +13 -0
- package/inspector/dist/assets/index-C8AKqc_i.css +1 -0
- package/inspector/dist/assets/roboto-latin-400-normal-BqEyEoaF.woff2 +0 -0
- package/inspector/dist/assets/roboto-latin-400-normal-D27F5sTu.woff +0 -0
- package/inspector/dist/assets/roboto-latin-500-normal-7RbcRiD8.woff2 +0 -0
- package/inspector/dist/assets/roboto-latin-500-normal-fsTFPL3E.woff +0 -0
- package/inspector/dist/assets/roboto-latin-700-normal-BZpUvMxY.woff2 +0 -0
- package/inspector/dist/assets/roboto-latin-700-normal-YuyVweIx.woff +0 -0
- package/inspector/dist/assets/roboto-mono-latin-400-normal-DBZPkcnn.woff +0 -0
- package/inspector/dist/assets/roboto-mono-latin-400-normal-GekRknry.woff2 +0 -0
- package/inspector/dist/assets/roboto-mono-latin-500-normal-D6j6AMzd.woff +0 -0
- package/inspector/dist/assets/roboto-mono-latin-500-normal-pxIOXZmA.woff2 +0 -0
- package/inspector/dist/assets/roboto-mono-latin-700-normal-CHYrj1DZ.woff +0 -0
- package/inspector/dist/assets/roboto-mono-latin-700-normal-foxDnrFi.woff2 +0 -0
- package/inspector/dist/index.html +2 -2
- package/package.json +1 -1
- package/inspector/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-700-normal-7sUh57Bg.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-mono-latin-700-normal-CNHXzs6v.woff +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-700-normal-Bth3BMcD.woff +0 -0
- package/inspector/dist/assets/ibm-plex-sans-latin-700-normal-Bxkt5Cjx.woff2 +0 -0
- package/inspector/dist/assets/index-BtcPa8nx.css +0 -1
- package/inspector/dist/assets/index-CImcmBVs.js +0 -10
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
export declare function setOutputDir(dir: string): void;
|
|
2
|
-
|
|
2
|
+
export declare function setProjectRoot(dir: string): void;
|
|
3
|
+
/** Absolute output directory (runs/ + chats/ live inside). */
|
|
3
4
|
export declare function getOutputDir(): string;
|
|
4
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* The consumer project root. Set explicitly at startup; falls back to the
|
|
7
|
+
* parent of the output dir (wrong for nested output dirs like
|
|
8
|
+
* kraftwerk-data/output — always pass projectRoot when known).
|
|
9
|
+
*/
|
|
5
10
|
export declare function getProjectRoot(): string;
|
|
@@ -4,16 +4,24 @@ import path from "node:path";
|
|
|
4
4
|
* Set once by startInspector() before any request is served.
|
|
5
5
|
*/
|
|
6
6
|
let outputDir = "";
|
|
7
|
+
let projectRoot = "";
|
|
7
8
|
export function setOutputDir(dir) {
|
|
8
9
|
outputDir = path.resolve(dir);
|
|
9
10
|
}
|
|
10
|
-
|
|
11
|
+
export function setProjectRoot(dir) {
|
|
12
|
+
projectRoot = path.resolve(dir);
|
|
13
|
+
}
|
|
14
|
+
/** Absolute output directory (runs/ + chats/ live inside). */
|
|
11
15
|
export function getOutputDir() {
|
|
12
16
|
if (!outputDir)
|
|
13
17
|
throw new Error("inspector context not initialized");
|
|
14
18
|
return outputDir;
|
|
15
19
|
}
|
|
16
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* The consumer project root. Set explicitly at startup; falls back to the
|
|
22
|
+
* parent of the output dir (wrong for nested output dirs like
|
|
23
|
+
* kraftwerk-data/output — always pass projectRoot when known).
|
|
24
|
+
*/
|
|
17
25
|
export function getProjectRoot() {
|
|
18
|
-
return path.dirname(getOutputDir());
|
|
26
|
+
return projectRoot || path.dirname(getOutputDir());
|
|
19
27
|
}
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workflow trigger for the web UI. The inspector stays decoupled from the
|
|
3
|
+
* framework runtime: it shells out to `npx kraftwerk run` in the consumer
|
|
4
|
+
* project, detached, with a pre-chosen --run-id so the browser can jump to
|
|
5
|
+
* /runs/<id> immediately and watch the (already polling) live timeline.
|
|
6
|
+
*
|
|
7
|
+
* Sandbox mode adds --sandbox: one Docker container per run, workflow
|
|
8
|
+
* mounted read-only, run dir bind-mounted back into output/ (see
|
|
9
|
+
* kraftwerk/src/runner/docker.ts).
|
|
10
|
+
*/
|
|
1
11
|
export declare function dockerStatus(): {
|
|
2
12
|
available: boolean;
|
|
3
13
|
image: boolean;
|
package/dist/inspector/runner.js
CHANGED
|
@@ -2,6 +2,8 @@ import { spawn, spawnSync } from "node:child_process";
|
|
|
2
2
|
import { mkdirSync, openSync, closeSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { getProjectRoot } from "./context.js";
|
|
5
|
+
import { RUN_ID_RE, safeRunDir } from "./runs.js";
|
|
6
|
+
import { newRunId } from "../workflow.js";
|
|
5
7
|
/**
|
|
6
8
|
* Workflow trigger for the web UI. The inspector stays decoupled from the
|
|
7
9
|
* framework runtime: it shells out to `npx kraftwerk run` in the consumer
|
|
@@ -12,10 +14,6 @@ import { getProjectRoot } from "./context.js";
|
|
|
12
14
|
* mounted read-only, run dir bind-mounted back into output/ (see
|
|
13
15
|
* kraftwerk/src/runner/docker.ts).
|
|
14
16
|
*/
|
|
15
|
-
function stamp(d = new Date()) {
|
|
16
|
-
const p = (n) => String(n).padStart(2, "0");
|
|
17
|
-
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}-${p(d.getSeconds())}`;
|
|
18
|
-
}
|
|
19
17
|
export function dockerStatus() {
|
|
20
18
|
const daemon = spawnSync("docker", ["version", "--format", "ok"], {
|
|
21
19
|
stdio: "ignore",
|
|
@@ -37,9 +35,9 @@ export function triggerRun(opts) {
|
|
|
37
35
|
if (!docker.image)
|
|
38
36
|
throw new Error('Image "kraftwerk-runner" missing — run `kraftwerk runner build`.');
|
|
39
37
|
}
|
|
40
|
-
const runId =
|
|
41
|
-
// Matches the CLI's --run-id resolution: output/<id
|
|
42
|
-
const runDir =
|
|
38
|
+
const runId = newRunId(opts.workflowName);
|
|
39
|
+
// Matches the CLI's --run-id resolution: <output dir>/runs/<id>.
|
|
40
|
+
const runDir = safeRunDir(runId);
|
|
43
41
|
mkdirSync(runDir, { recursive: true });
|
|
44
42
|
const log = openSync(path.join(runDir, "trigger.log"), "a");
|
|
45
43
|
const args = ["kraftwerk", "run", "--yes", "--run-id", runId];
|
|
@@ -60,7 +58,7 @@ export function triggerRun(opts) {
|
|
|
60
58
|
}
|
|
61
59
|
export function stopRun(runId) {
|
|
62
60
|
// Sandboxed runs run in container kw-<runId>; docker stop ends them.
|
|
63
|
-
if (
|
|
61
|
+
if (!RUN_ID_RE.test(runId))
|
|
64
62
|
return false;
|
|
65
63
|
return (spawnSync("docker", ["stop", `kw-${runId}`], { stdio: "ignore", timeout: 30_000 }).status === 0);
|
|
66
64
|
}
|
package/dist/inspector/runs.d.ts
CHANGED
|
@@ -49,6 +49,8 @@ export interface RunDetail extends RunListItem {
|
|
|
49
49
|
files: FileView[];
|
|
50
50
|
}
|
|
51
51
|
export declare function listRuns(): Promise<RunListItem[]>;
|
|
52
|
+
/** "2026-08-25-1432-07-hello" (runs/ subdir) or legacy "run-…" (output root). */
|
|
53
|
+
export declare const RUN_ID_RE: RegExp;
|
|
52
54
|
export declare function safeRunDir(id: string): string;
|
|
53
55
|
export declare function getRun(id: string): Promise<RunDetail | null>;
|
|
54
56
|
export declare function readRunFile(id: string, name: string): Promise<{
|
package/dist/inspector/runs.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { getOutputDir } from "./context.js";
|
|
4
4
|
/**
|
|
5
5
|
* Filesystem + trace.jsonl reading for the inspector. The output directory
|
|
6
|
-
* holds one folder per run (
|
|
6
|
+
* holds one folder per run (runs/<stamp>-<workflow>, legacy run-* at the root);
|
|
7
7
|
* trace.jsonl written by the framework plus the run's working files.
|
|
8
8
|
*/
|
|
9
9
|
/** A run with no trace update for this long and no summary counts as aborted. */
|
|
@@ -158,15 +158,14 @@ function shortenPath(p) {
|
|
|
158
158
|
return parts.length > 2 ? parts.slice(-2).join("/") : p;
|
|
159
159
|
}
|
|
160
160
|
export async function listRuns() {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
161
|
+
// New runs live under <output>/runs/; legacy run-* folders sit at the root.
|
|
162
|
+
const current = (await fs.readdir(path.join(getOutputDir(), "runs")).catch(() => []))
|
|
163
|
+
.filter((e) => RUN_ID_RE.test(e) && !e.startsWith("run-"));
|
|
164
|
+
const legacy = (await fs.readdir(getOutputDir()).catch(() => []))
|
|
165
|
+
.filter((e) => e.startsWith("run-") && RUN_ID_RE.test(e));
|
|
166
|
+
const entries = [...current, ...legacy];
|
|
168
167
|
const items = await Promise.all(entries.map(async (id) => {
|
|
169
|
-
const runDir =
|
|
168
|
+
const runDir = safeRunDir(id);
|
|
170
169
|
const st = await fs.stat(runDir).catch(() => null);
|
|
171
170
|
if (!st?.isDirectory())
|
|
172
171
|
return null;
|
|
@@ -188,12 +187,18 @@ export async function listRuns() {
|
|
|
188
187
|
costUsd: summary?.total?.costUsd,
|
|
189
188
|
};
|
|
190
189
|
}));
|
|
191
|
-
|
|
190
|
+
// Newest first; strip the legacy "run-" prefix so both id styles compare by stamp.
|
|
191
|
+
const stampOf = (r) => r.id.replace(/^run-/, "");
|
|
192
|
+
return items.filter(Boolean).sort((a, b) => stampOf(b).localeCompare(stampOf(a)));
|
|
192
193
|
}
|
|
194
|
+
/** "2026-08-25-1432-07-hello" (runs/ subdir) or legacy "run-…" (output root). */
|
|
195
|
+
export const RUN_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
193
196
|
export function safeRunDir(id) {
|
|
194
|
-
if (
|
|
197
|
+
if (!RUN_ID_RE.test(id))
|
|
195
198
|
throw new Error("invalid run id");
|
|
196
|
-
return
|
|
199
|
+
return id.startsWith("run-")
|
|
200
|
+
? path.join(getOutputDir(), id)
|
|
201
|
+
: path.join(getOutputDir(), "runs", id);
|
|
197
202
|
}
|
|
198
203
|
export async function getRun(id) {
|
|
199
204
|
const runDir = safeRunDir(id);
|
|
@@ -3,6 +3,8 @@ export interface InspectorOptions {
|
|
|
3
3
|
outputDir: string;
|
|
4
4
|
staticDir: string;
|
|
5
5
|
port: number;
|
|
6
|
+
/** Consumer project root; defaults to the parent of outputDir. */
|
|
7
|
+
projectRoot?: string;
|
|
6
8
|
}
|
|
7
9
|
/** Start the server; resolves once it listens. Runs until the process ends. */
|
|
8
10
|
export declare function startInspector(opts: InspectorOptions): Promise<http.Server>;
|
package/dist/inspector/server.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { promises as fs } from "node:fs";
|
|
2
2
|
import http from "node:http";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { setOutputDir, getOutputDir } from "./context.js";
|
|
4
|
+
import { setOutputDir, setProjectRoot, getOutputDir, getProjectRoot } from "./context.js";
|
|
5
|
+
import { resolveProject } from "../config.js";
|
|
5
6
|
import { listRuns, getRun, readRunFile } from "./runs.js";
|
|
6
7
|
import { listWorkflows, getWorkflow } from "./workflows.js";
|
|
7
8
|
import { dockerStatus, triggerRun, stopRun } from "./runner.js";
|
|
8
|
-
import { cancelChat, createChat, getChat, listChats, postMessage, resolvePermission, subscribeChat, } from "./chat/sessions.js";
|
|
9
|
+
import { cancelChat, createChat, deleteChat, getChat, listChats, postMessage, resolvePermission, subscribeChat, } from "./chat/sessions.js";
|
|
9
10
|
import { bundleDetail, conceptDetail, createBundle, knowledgeIndex, putConcept, verifyFromUi, } from "./knowledge.js";
|
|
10
11
|
import { deleteMember, getMember, listMembers, saveMember, teamRoot, } from "./team.js";
|
|
12
|
+
import { listSkills } from "./skills.js";
|
|
11
13
|
import { deleteRoutine, routineStatuses, runRoutineNow, saveRoutine, startRoutineScheduler, } from "./routines.js";
|
|
12
14
|
/**
|
|
13
15
|
* The inspector server: a plain node:http server with no dependencies.
|
|
@@ -53,9 +55,29 @@ function readBody(req) {
|
|
|
53
55
|
req.on("error", reject);
|
|
54
56
|
});
|
|
55
57
|
}
|
|
58
|
+
/** Package version, read once. Works from src/ (tsx) and dist/ alike. */
|
|
59
|
+
let pkgVersion;
|
|
60
|
+
async function getPkgVersion() {
|
|
61
|
+
if (pkgVersion === undefined) {
|
|
62
|
+
try {
|
|
63
|
+
const raw = await fs.readFile(new URL("../../package.json", import.meta.url), "utf8");
|
|
64
|
+
pkgVersion = JSON.parse(raw).version ?? "";
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
pkgVersion = "";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return pkgVersion;
|
|
71
|
+
}
|
|
56
72
|
async function handleApi(req, res, url) {
|
|
57
73
|
const seg = url.pathname.split("/").filter(Boolean); // ["api", ...]
|
|
58
74
|
const method = req.method ?? "GET";
|
|
75
|
+
// GET /api/meta — package version + project name ("environment") for the UI
|
|
76
|
+
if (seg.length === 2 && seg[1] === "meta" && method === "GET") {
|
|
77
|
+
const project = await resolveProject(getProjectRoot()).catch(() => null);
|
|
78
|
+
const projectName = project?.config.name ?? (project ? path.basename(project.root) : "");
|
|
79
|
+
return json(res, { version: await getPkgVersion(), projectName, projectIcon: project?.config.icon ?? "" });
|
|
80
|
+
}
|
|
59
81
|
// GET /api/runs
|
|
60
82
|
if (seg.length === 2 && seg[1] === "runs" && method === "GET") {
|
|
61
83
|
return json(res, { outputDir: getOutputDir(), runs: await listRuns() });
|
|
@@ -227,6 +249,10 @@ async function handleApi(req, res, url) {
|
|
|
227
249
|
return json(res, { error: err.message }, 400);
|
|
228
250
|
}
|
|
229
251
|
}
|
|
252
|
+
// GET /api/skills — discovered skills (project + user .claude/skills)
|
|
253
|
+
if (seg.length === 2 && seg[1] === "skills" && method === "GET") {
|
|
254
|
+
return json(res, { skills: await listSkills() });
|
|
255
|
+
}
|
|
230
256
|
// GET/POST /api/team — member list / create a member
|
|
231
257
|
if (seg.length === 2 && seg[1] === "team") {
|
|
232
258
|
if (method === "GET") {
|
|
@@ -340,6 +366,16 @@ async function handleApi(req, res, url) {
|
|
|
340
366
|
}
|
|
341
367
|
}
|
|
342
368
|
}
|
|
369
|
+
// DELETE /api/chats/:id
|
|
370
|
+
if (seg.length === 3 && seg[1] === "chats" && method === "DELETE") {
|
|
371
|
+
try {
|
|
372
|
+
const result = await deleteChat(seg[2]);
|
|
373
|
+
return json(res, result, result.error ? 404 : 200);
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
return json(res, { error: "invalid chat id" }, 400);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
343
379
|
// GET /api/chats/:id
|
|
344
380
|
if (seg.length === 3 && seg[1] === "chats" && method === "GET") {
|
|
345
381
|
try {
|
|
@@ -438,6 +474,8 @@ async function serveStatic(res, staticDir, pathname) {
|
|
|
438
474
|
/** Start the server; resolves once it listens. Runs until the process ends. */
|
|
439
475
|
export function startInspector(opts) {
|
|
440
476
|
setOutputDir(opts.outputDir);
|
|
477
|
+
if (opts.projectRoot)
|
|
478
|
+
setProjectRoot(opts.projectRoot);
|
|
441
479
|
startRoutineScheduler();
|
|
442
480
|
const server = http.createServer(async (req, res) => {
|
|
443
481
|
try {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills: reusable instruction packages for chats, discovered from the
|
|
3
|
+
* Claude-style skill layout — one folder per skill holding a SKILL.md with
|
|
4
|
+
* YAML frontmatter (name, description). Two roots are scanned:
|
|
5
|
+
*
|
|
6
|
+
* <projectRoot>/.claude/skills/<name>/SKILL.md (git-tracked, per project)
|
|
7
|
+
* ~/.claude/skills/<name>/SKILL.md (personal, per user)
|
|
8
|
+
*
|
|
9
|
+
* A project skill shadows a user skill with the same name. Skills are
|
|
10
|
+
* surfaced to every chat as context (and expanded on /<name> invocation),
|
|
11
|
+
* so they work identically on claude, codex, and pi.
|
|
12
|
+
*/
|
|
13
|
+
export interface SkillInfo {
|
|
14
|
+
/** Frontmatter `name`, falling back to the folder name. */
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
/** Absolute path to the SKILL.md file. */
|
|
18
|
+
path: string;
|
|
19
|
+
/** Absolute path to the skill folder (for harnesses that take a dir). */
|
|
20
|
+
dir: string;
|
|
21
|
+
source: "project" | "user";
|
|
22
|
+
}
|
|
23
|
+
/** All discovered skills; a project skill shadows a same-named user skill. */
|
|
24
|
+
export declare function listSkills(): Promise<SkillInfo[]>;
|
|
25
|
+
/** Read a skill's SKILL.md contents (empty string when unreadable). */
|
|
26
|
+
export declare function readSkill(skill: SkillInfo): Promise<string>;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { parse } from "yaml";
|
|
5
|
+
import { getProjectRoot } from "./context.js";
|
|
6
|
+
const NAME_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/i;
|
|
7
|
+
function frontmatter(md) {
|
|
8
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---(\r?\n|$)/.exec(md);
|
|
9
|
+
if (!m)
|
|
10
|
+
return {};
|
|
11
|
+
try {
|
|
12
|
+
const parsed = parse(m[1]);
|
|
13
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function scanRoot(root, source) {
|
|
20
|
+
let entries;
|
|
21
|
+
try {
|
|
22
|
+
entries = (await fs.readdir(root, { withFileTypes: true }))
|
|
23
|
+
.filter((e) => e.isDirectory())
|
|
24
|
+
.map((e) => e.name);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
const skills = await Promise.all(entries.map(async (dirName) => {
|
|
30
|
+
const dir = path.join(root, dirName);
|
|
31
|
+
const file = path.join(dir, "SKILL.md");
|
|
32
|
+
let md;
|
|
33
|
+
try {
|
|
34
|
+
md = await fs.readFile(file, "utf8");
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const fm = frontmatter(md);
|
|
40
|
+
const name = String(fm.name ?? dirName).trim() || dirName;
|
|
41
|
+
if (!NAME_RE.test(name))
|
|
42
|
+
return null;
|
|
43
|
+
return {
|
|
44
|
+
name,
|
|
45
|
+
description: String(fm.description ?? "").trim(),
|
|
46
|
+
path: file,
|
|
47
|
+
dir,
|
|
48
|
+
source,
|
|
49
|
+
};
|
|
50
|
+
}));
|
|
51
|
+
return skills.filter(Boolean).sort((a, b) => a.name.localeCompare(b.name));
|
|
52
|
+
}
|
|
53
|
+
/** All discovered skills; a project skill shadows a same-named user skill. */
|
|
54
|
+
export async function listSkills() {
|
|
55
|
+
const [project, user] = await Promise.all([
|
|
56
|
+
scanRoot(path.join(getProjectRoot(), ".claude", "skills"), "project"),
|
|
57
|
+
scanRoot(path.join(os.homedir(), ".claude", "skills"), "user"),
|
|
58
|
+
]);
|
|
59
|
+
const seen = new Set(project.map((s) => s.name.toLowerCase()));
|
|
60
|
+
return [...project, ...user.filter((s) => !seen.has(s.name.toLowerCase()))];
|
|
61
|
+
}
|
|
62
|
+
/** Read a skill's SKILL.md contents (empty string when unreadable). */
|
|
63
|
+
export function readSkill(skill) {
|
|
64
|
+
return fs.readFile(skill.path, "utf8").catch(() => "");
|
|
65
|
+
}
|
package/dist/inspector/team.d.ts
CHANGED
|
@@ -23,6 +23,11 @@ export interface TeamMember {
|
|
|
23
23
|
workflows: string[];
|
|
24
24
|
/** Knowledge bundles (OKF) this member works with. */
|
|
25
25
|
knowledge: string[];
|
|
26
|
+
/**
|
|
27
|
+
* Skill allowlist (names from .claude/skills, project or user level).
|
|
28
|
+
* Absent = all discovered skills; empty list = no skills.
|
|
29
|
+
*/
|
|
30
|
+
skills?: string[];
|
|
26
31
|
}
|
|
27
32
|
export interface TeamMemberDetail extends TeamMember {
|
|
28
33
|
/** Contents of system.md — the member's system prompt. */
|
|
@@ -44,6 +49,8 @@ export interface SaveMemberInput {
|
|
|
44
49
|
effort?: string;
|
|
45
50
|
workflows?: string[];
|
|
46
51
|
knowledge?: string[];
|
|
52
|
+
/** Omit for "all skills"; a list (possibly empty) restricts to those names. */
|
|
53
|
+
skills?: string[];
|
|
47
54
|
system?: string;
|
|
48
55
|
}
|
|
49
56
|
export declare function saveMember(input: SaveMemberInput): Promise<TeamMemberDetail>;
|
package/dist/inspector/team.js
CHANGED
|
@@ -50,6 +50,7 @@ function normalize(slug, raw) {
|
|
|
50
50
|
...(raw.effort ? { effort: String(raw.effort) } : {}),
|
|
51
51
|
workflows: Array.isArray(raw.workflows) ? raw.workflows.map(String) : [],
|
|
52
52
|
knowledge: Array.isArray(raw.knowledge) ? raw.knowledge.map(String) : [],
|
|
53
|
+
...(Array.isArray(raw.skills) ? { skills: raw.skills.map(String) } : {}),
|
|
53
54
|
};
|
|
54
55
|
}
|
|
55
56
|
export async function listMembers() {
|
|
@@ -108,6 +109,7 @@ export async function saveMember(input) {
|
|
|
108
109
|
...(input.effort ? { effort: input.effort } : {}),
|
|
109
110
|
workflows: (input.workflows ?? []).map(String),
|
|
110
111
|
knowledge: (input.knowledge ?? []).map(String),
|
|
112
|
+
...(input.skills ? { skills: input.skills.map(String) } : {}),
|
|
111
113
|
};
|
|
112
114
|
await fs.writeFile(path.join(dir, "agent.yml"), stringify(yml));
|
|
113
115
|
await fs.writeFile(path.join(dir, "system.md"), (input.system ?? "").trim() + "\n");
|
package/dist/runner/docker.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
export interface SandboxOptions {
|
|
2
2
|
/** Consumer project root (where output/ lives). */
|
|
3
3
|
projectRoot: string;
|
|
4
|
+
/** Output directory; defaults to <projectRoot>/output (kraftwerk.yml `output:` may differ). */
|
|
5
|
+
outputDir?: string;
|
|
4
6
|
/** Workflow folder (folder mode) or .yml file path. */
|
|
5
7
|
workflowPath: string;
|
|
6
8
|
/** Workflow name as declared in the YAML (used for `kraftwerk run <name>`). */
|
|
7
9
|
workflowName: string;
|
|
8
10
|
request: string;
|
|
9
|
-
/** Pre-chosen run id
|
|
11
|
+
/** Pre-chosen run id; generated from workflow name + stamp when omitted. */
|
|
10
12
|
runId?: string;
|
|
11
13
|
/** Forward the host SSH agent + known_hosts into the container. */
|
|
12
14
|
ssh?: boolean;
|
package/dist/runner/docker.js
CHANGED
|
@@ -4,7 +4,7 @@ import { existsSync } from "node:fs";
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import {
|
|
7
|
+
import { newRunId, runDirFor } from "../workflow.js";
|
|
8
8
|
/**
|
|
9
9
|
* Docker sandbox runner: one container per workflow run.
|
|
10
10
|
*
|
|
@@ -57,8 +57,8 @@ export async function runSandboxed(opts) {
|
|
|
57
57
|
if (!imageExists()) {
|
|
58
58
|
throw new Error(`Image "${IMAGE}" not found — run \`kraftwerk runner build\` first.`);
|
|
59
59
|
}
|
|
60
|
-
const runId = opts.runId ??
|
|
61
|
-
const runDir = path.join(opts.projectRoot, "output", runId);
|
|
60
|
+
const runId = opts.runId ?? newRunId(opts.workflowName);
|
|
61
|
+
const runDir = runDirFor(opts.outputDir ?? path.join(opts.projectRoot, "output"), runId);
|
|
62
62
|
await mkdir(runDir, { recursive: true });
|
|
63
63
|
const containerName = `kw-${runId}`;
|
|
64
64
|
// Folder mode and single-file mode both mount under src/workflows/ so
|
|
@@ -73,8 +73,8 @@ export async function runSandboxed(opts) {
|
|
|
73
73
|
"--memory", opts.memory ?? "2g",
|
|
74
74
|
"--cpus", opts.cpus ?? "2",
|
|
75
75
|
"-v", `${wfAbs}:${wfTarget}:ro`,
|
|
76
|
-
"-v", `${runDir}:/work/output/${runId}`,
|
|
77
|
-
"-e", `KRAFTWERK_RUN_DIR=/work/output/${runId}`,
|
|
76
|
+
"-v", `${runDir}:/work/output/runs/${runId}`,
|
|
77
|
+
"-e", `KRAFTWERK_RUN_DIR=/work/output/runs/${runId}`,
|
|
78
78
|
];
|
|
79
79
|
const envFile = path.join(opts.projectRoot, ENV_FILE);
|
|
80
80
|
if (existsSync(envFile))
|
package/dist/workflow.d.ts
CHANGED
|
@@ -22,3 +22,10 @@ export interface WorkflowDefinition {
|
|
|
22
22
|
}
|
|
23
23
|
/** Local-time run-folder stamp: "2026-08-13-1432-07" (sortable, no colons). */
|
|
24
24
|
export declare function runStamp(date?: Date): string;
|
|
25
|
+
/** Run-folder name: "<stamp>-<workflow-slug>", e.g. "2026-08-25-1432-07-hello". */
|
|
26
|
+
export declare function newRunId(workflowName: string, date?: Date): string;
|
|
27
|
+
/**
|
|
28
|
+
* Where a run's folder lives: <output>/runs/<id>. Legacy ids ("run-<stamp>",
|
|
29
|
+
* pre-runs-subfolder) stay at the output root so old runs remain readable.
|
|
30
|
+
*/
|
|
31
|
+
export declare function runDirFor(outputDir: string, runId: string): string;
|
package/dist/workflow.js
CHANGED
|
@@ -3,9 +3,28 @@
|
|
|
3
3
|
* A workflow owns its run directory, roster, prompts, and gates; the
|
|
4
4
|
* framework only provides the phase runner and the CLI dispatch.
|
|
5
5
|
*/
|
|
6
|
+
import path from "node:path";
|
|
6
7
|
/** Local-time run-folder stamp: "2026-08-13-1432-07" (sortable, no colons). */
|
|
7
8
|
export function runStamp(date = new Date()) {
|
|
8
9
|
const pad = (n) => String(n).padStart(2, "0");
|
|
9
10
|
return (`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}` +
|
|
10
11
|
`-${pad(date.getHours())}${pad(date.getMinutes())}-${pad(date.getSeconds())}`);
|
|
11
12
|
}
|
|
13
|
+
/** Run-folder name: "<stamp>-<workflow-slug>", e.g. "2026-08-25-1432-07-hello". */
|
|
14
|
+
export function newRunId(workflowName, date = new Date()) {
|
|
15
|
+
const slug = workflowName
|
|
16
|
+
.toLowerCase()
|
|
17
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
18
|
+
.replace(/^-+|-+$/g, "")
|
|
19
|
+
.slice(0, 40) || "workflow";
|
|
20
|
+
return `${runStamp(date)}-${slug}`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Where a run's folder lives: <output>/runs/<id>. Legacy ids ("run-<stamp>",
|
|
24
|
+
* pre-runs-subfolder) stay at the output root so old runs remain readable.
|
|
25
|
+
*/
|
|
26
|
+
export function runDirFor(outputDir, runId) {
|
|
27
|
+
return runId.startsWith("run-")
|
|
28
|
+
? path.join(outputDir, runId)
|
|
29
|
+
: path.join(outputDir, "runs", runId);
|
|
30
|
+
}
|
package/dist/yaml.js
CHANGED
|
@@ -6,7 +6,7 @@ import { envelopeContract } from "./envelope.js";
|
|
|
6
6
|
import { checkScript, containsText, fileNonEmpty, slotsFilled } from "./gates.js";
|
|
7
7
|
import { Run } from "./run.js";
|
|
8
8
|
import { summaryTable } from "./stats.js";
|
|
9
|
-
import {
|
|
9
|
+
import { newRunId } from "./workflow.js";
|
|
10
10
|
/**
|
|
11
11
|
* YAML-configured workflows (v1: a linear sequence of gated agent steps).
|
|
12
12
|
* Vocabulary borrows from GitHub Actions: `steps`, `runs-on` (the harness),
|
|
@@ -266,7 +266,7 @@ export async function loadWorkflow(givenPath) {
|
|
|
266
266
|
// the run directory upfront so it can mount/watch it by name.
|
|
267
267
|
const runDir = process.env.KRAFTWERK_RUN_DIR
|
|
268
268
|
? path.resolve(process.env.KRAFTWERK_RUN_DIR)
|
|
269
|
-
: path.resolve("output",
|
|
269
|
+
: path.resolve("output", "runs", newRunId(raw.name));
|
|
270
270
|
await mkdir(runDir, { recursive: true });
|
|
271
271
|
const run = new Run({
|
|
272
272
|
runDir,
|