@netnodeag/kraftwerk 0.35.0 → 0.37.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 +65 -20
- package/dist/cli/doctor.js +18 -1
- package/dist/cli/init.js +19 -12
- package/dist/cli/kraftwerk.js +3 -0
- package/dist/cli/projects.d.ts +14 -0
- package/dist/cli/projects.js +1 -1
- package/dist/cli/repos.d.ts +2 -0
- package/dist/cli/repos.js +115 -0
- package/dist/cli/routines.d.ts +10 -0
- package/dist/cli/routines.js +10 -10
- package/dist/config.d.ts +29 -2
- package/dist/config.js +56 -2
- package/dist/inspector/{team.d.ts → agents.d.ts} +21 -12
- package/dist/inspector/{team.js → agents.js} +34 -19
- package/dist/inspector/chat/backend.d.ts +1 -1
- package/dist/inspector/chat/sessions.js +72 -45
- package/dist/inspector/chat/store.js +13 -1
- package/dist/inspector/chat/types.d.ts +2 -2
- package/dist/inspector/git.d.ts +14 -0
- package/dist/inspector/git.js +11 -9
- package/dist/inspector/instances.d.ts +12 -0
- package/dist/inspector/instances.js +28 -0
- package/dist/inspector/repos.d.ts +118 -0
- package/dist/inspector/repos.js +249 -0
- package/dist/inspector/routines.d.ts +4 -4
- package/dist/inspector/routines.js +11 -11
- package/dist/inspector/search.d.ts +25 -0
- package/dist/inspector/search.js +31 -0
- package/dist/inspector/server.js +77 -30
- package/dist/inspector/settings.d.ts +7 -1
- package/dist/inspector/settings.js +32 -1
- package/dist/inspector/skills.d.ts +6 -6
- package/dist/inspector/skills.js +8 -8
- package/inspector/dist/assets/{editor-BABv2LmA.js → editor-vM6JSj5h.js} +3 -3
- package/inspector/dist/assets/{index-CCACy6uF.js → index-BAAjszPI.js} +1 -1
- package/inspector/dist/assets/{index-DuwmzZAr.js → index-BKcOp35r.js} +1 -1
- package/inspector/dist/assets/index-Ba8VU6Wz.js +217 -0
- package/inspector/dist/assets/{index-Ca9Qs5LZ.js → index-Bb08wK-l.js} +1 -1
- package/inspector/dist/assets/{index-ByWpqzAI.js → index-BrCwDkJZ.js} +1 -1
- package/inspector/dist/assets/{index-hkVov8T4.js → index-Bs1U0rWW.js} +1 -1
- package/inspector/dist/assets/{index-CdwTmXTV.js → index-C1UXlW32.js} +1 -1
- package/inspector/dist/assets/{index-HBdsdJhD.js → index-CQ2XKqmD.js} +1 -1
- package/inspector/dist/assets/{index-DCR7u4Xa.js → index-CReepirH.js} +1 -1
- package/inspector/dist/assets/{index-CzPQygR7.js → index-CUGfxoYt.js} +1 -1
- package/inspector/dist/assets/{index-sLxulPOm.js → index-CaKmxcD6.js} +1 -1
- package/inspector/dist/assets/{index-Bmm4zJNU.js → index-CtMGH62C.js} +1 -1
- package/inspector/dist/assets/{index-BXspF2KR.css → index-D4_mLTSH.css} +1 -1
- package/inspector/dist/assets/{index-BbTer62d.js → index-D_11IZBV.js} +1 -1
- package/inspector/dist/assets/{index-wkYwz0Tl.js → index-DfZT0IoM.js} +1 -1
- package/inspector/dist/assets/{index-Gx93brFq.js → index-DykDNrYm.js} +1 -1
- package/inspector/dist/assets/{index-DuPp3MUc.js → index-MWv5H1oP.js} +1 -1
- package/inspector/dist/assets/{index-CjlJeW9z.js → index-oIQqfbwm.js} +1 -1
- package/inspector/dist/assets/{index-DbWUNbt-.js → index-yGZsomcy.js} +1 -1
- package/inspector/dist/index.html +2 -2
- package/package.json +1 -1
- package/schema/kraftwerk.schema.json +15 -0
- package/inspector/dist/assets/index-uEig4--d.js +0 -217
package/dist/config.js
CHANGED
|
@@ -18,7 +18,7 @@ import { parse } from "yaml";
|
|
|
18
18
|
* workflows: src/workflows # workflows root, relative to the file
|
|
19
19
|
* output: output # run-artifact directory, relative to the file
|
|
20
20
|
* knowledge: knowledge # OKF knowledge-bundle root, relative to the file
|
|
21
|
-
* agents: agents #
|
|
21
|
+
* agents: agents # agent-definition root, relative to the file
|
|
22
22
|
* skills: skills # workspace skill root, relative to the file
|
|
23
23
|
* switcher: # other kraftwerk workspaces, linked from the header dropdown
|
|
24
24
|
* - name: other space
|
|
@@ -28,6 +28,8 @@ import { parse } from "yaml";
|
|
|
28
28
|
* remote: origin
|
|
29
29
|
* interval: 300 # seconds between background fetches
|
|
30
30
|
* autosync: pull # off | pull
|
|
31
|
+
* repos: # git repositories the agents work on (absent = off, bare key = on)
|
|
32
|
+
* root: kraftwerk-data/repos # where clones land, relative to the file. Default: repos
|
|
31
33
|
*/
|
|
32
34
|
/** Stable, versionless URL of the workflow JSON schema (editor validation). */
|
|
33
35
|
export const SCHEMA_URL = "https://raw.githubusercontent.com/NETNODEAG/kraftwerk/main/kraftwerk/schema/workflow.schema.json";
|
|
@@ -35,6 +37,30 @@ export const SCHEMA_URL = "https://raw.githubusercontent.com/NETNODEAG/kraftwerk
|
|
|
35
37
|
export const CONFIG_SCHEMA_URL = "https://raw.githubusercontent.com/NETNODEAG/kraftwerk/main/kraftwerk/schema/kraftwerk.schema.json";
|
|
36
38
|
export const CONFIG_FILENAMES = ["kraftwerk.yml", "kraftwerk.yaml"];
|
|
37
39
|
const WORKFLOW_ROOT_CANDIDATES = ["src/workflows", "workflows"];
|
|
40
|
+
/** Where clones land when `repos.root` is not set. */
|
|
41
|
+
export const REPOS_DEFAULT_ROOT = "repos";
|
|
42
|
+
/**
|
|
43
|
+
* Absolute repos root when the feature is on, undefined otherwise. The one
|
|
44
|
+
* place that reads the block, so the sync exclude, the doctor check, the
|
|
45
|
+
* settings save and the module itself cannot disagree.
|
|
46
|
+
*/
|
|
47
|
+
export function reposRootFor(project) {
|
|
48
|
+
const r = project.config.repos;
|
|
49
|
+
if (!r || r.enabled === false)
|
|
50
|
+
return undefined;
|
|
51
|
+
return path.resolve(project.root, r.root ?? REPOS_DEFAULT_ROOT);
|
|
52
|
+
}
|
|
53
|
+
/** A directory as a .gitignore entry: relative, forward slashes, no trailing slash; undefined outside the root. */
|
|
54
|
+
export function ignoreEntryFor(projectRoot, dir) {
|
|
55
|
+
const rel = path.relative(projectRoot, path.resolve(projectRoot, dir)).split(path.sep).join("/");
|
|
56
|
+
if (!rel || rel === "." || rel.startsWith(".."))
|
|
57
|
+
return undefined;
|
|
58
|
+
return rel;
|
|
59
|
+
}
|
|
60
|
+
/** True when .gitignore text already covers the entry (with or without a leading or trailing slash). */
|
|
61
|
+
export function gitignoreHas(text, entry) {
|
|
62
|
+
return text.split("\n").some((l) => l.trim().replace(/^\//, "").replace(/\/$/, "") === entry);
|
|
63
|
+
}
|
|
38
64
|
const exists = async (p) => !!(await stat(p).catch(() => null));
|
|
39
65
|
/** True when the path exists and is a directory. */
|
|
40
66
|
export const isDir = async (p) => (await stat(p).catch(() => null))?.isDirectory() ?? false;
|
|
@@ -87,7 +113,7 @@ export async function resolveProject(cwd) {
|
|
|
87
113
|
const root = gitFallback ?? start;
|
|
88
114
|
return { root, config: {}, outputDir: path.join(root, "output") };
|
|
89
115
|
}
|
|
90
|
-
const KNOWN_KEYS = ["name", "icon", "port", "workflows", "output", "knowledge", "agents", "skills", "switcher", "git"];
|
|
116
|
+
const KNOWN_KEYS = ["name", "icon", "port", "workflows", "output", "knowledge", "agents", "skills", "switcher", "git", "repos"];
|
|
91
117
|
async function loadConfig(configPath) {
|
|
92
118
|
let raw;
|
|
93
119
|
try {
|
|
@@ -123,6 +149,11 @@ async function loadConfig(configPath) {
|
|
|
123
149
|
config[key] = {};
|
|
124
150
|
validateGit(configPath, config[key]);
|
|
125
151
|
}
|
|
152
|
+
else if (key === "repos") {
|
|
153
|
+
if (config[key] === null)
|
|
154
|
+
config[key] = {};
|
|
155
|
+
validateRepos(configPath, config[key]);
|
|
156
|
+
}
|
|
126
157
|
else if (typeof config[key] !== "string") {
|
|
127
158
|
throw new Error(`${path.basename(configPath)}: ${key} must be a string`);
|
|
128
159
|
}
|
|
@@ -182,3 +213,26 @@ function validateGit(configPath, value) {
|
|
|
182
213
|
throw new Error(`${file}: git.autosync must be "off" or "pull"`);
|
|
183
214
|
}
|
|
184
215
|
}
|
|
216
|
+
function validateRepos(configPath, value) {
|
|
217
|
+
const file = path.basename(configPath);
|
|
218
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
219
|
+
throw new Error(`${file}: repos must be a mapping (enabled, root)`);
|
|
220
|
+
}
|
|
221
|
+
const r = value;
|
|
222
|
+
for (const key of Object.keys(r)) {
|
|
223
|
+
if (!["enabled", "root"].includes(key)) {
|
|
224
|
+
throw new Error(`${file}: repos.${key} is unknown (allowed: enabled, root)`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (r.enabled !== undefined && typeof r.enabled !== "boolean") {
|
|
228
|
+
throw new Error(`${file}: repos.enabled must be true or false`);
|
|
229
|
+
}
|
|
230
|
+
if (r.root !== undefined && (typeof r.root !== "string" || !r.root.trim())) {
|
|
231
|
+
throw new Error(`${file}: repos.root must be a non-empty string`);
|
|
232
|
+
}
|
|
233
|
+
// The root is what `repos remove --force` deletes under, so it must never
|
|
234
|
+
// be the project itself or anything outside it.
|
|
235
|
+
if (typeof r.root === "string" && !ignoreEntryFor(path.dirname(configPath), r.root.trim())) {
|
|
236
|
+
throw new Error(`${file}: repos.root must be a directory inside the project (not ".", ".." or an absolute path outside it)`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import type { ChatAgentId } from "./chat/types.js";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Agents: persistent agents, defined on the filesystem. Each one
|
|
4
4
|
* is one folder under the project's agents/ root:
|
|
5
5
|
*
|
|
6
6
|
* agents/<slug>/agent.yml # name, emoji, harness, model, effort, workflows
|
|
7
|
-
* agents/<slug>/system.md # the
|
|
7
|
+
* agents/<slug>/system.md # the agent's system prompt / role description
|
|
8
8
|
*
|
|
9
9
|
* Definitions are git-tracked project config (like workflows/), not run
|
|
10
10
|
* state — sessions with a member are ordinary chats scoped to it.
|
|
11
11
|
*/
|
|
12
12
|
export declare const HARNESSES: ChatAgentId[];
|
|
13
13
|
export declare const EFFORTS: readonly ["low", "medium", "high", "xhigh", "max"];
|
|
14
|
-
export interface
|
|
14
|
+
export interface Agent {
|
|
15
15
|
slug: string;
|
|
16
16
|
name: string;
|
|
17
17
|
emoji: string;
|
|
@@ -33,17 +33,26 @@ export interface TeamMember {
|
|
|
33
33
|
/** Archived: hidden from the active roster, restorable any time. */
|
|
34
34
|
archived?: boolean;
|
|
35
35
|
}
|
|
36
|
-
|
|
36
|
+
/** What the ⌘K palette needs to find and show an agent — kept in the project registry. */
|
|
37
|
+
export interface AgentSummary {
|
|
38
|
+
slug: string;
|
|
39
|
+
name: string;
|
|
40
|
+
emoji: string;
|
|
41
|
+
description?: string;
|
|
42
|
+
group?: string;
|
|
43
|
+
}
|
|
44
|
+
export declare const toSummary: (a: Agent) => AgentSummary;
|
|
45
|
+
export interface AgentDetail extends Agent {
|
|
37
46
|
/** Contents of system.md — the member's system prompt. */
|
|
38
47
|
system: string;
|
|
39
48
|
}
|
|
40
|
-
export declare function
|
|
41
|
-
export declare function
|
|
49
|
+
export declare function agentsRoot(): Promise<string>;
|
|
50
|
+
export declare function safeAgentSlug(slug: string): string;
|
|
42
51
|
/** Derive a slug from a display name: "Max Müller" -> "max-mueller"-ish. */
|
|
43
52
|
export declare function slugFromName(name: string): string;
|
|
44
|
-
export declare function
|
|
45
|
-
export declare function
|
|
46
|
-
export interface
|
|
53
|
+
export declare function listAgents(): Promise<Agent[]>;
|
|
54
|
+
export declare function getAgent(slug: string): Promise<AgentDetail | null>;
|
|
55
|
+
export interface SaveAgentInput {
|
|
47
56
|
slug?: string;
|
|
48
57
|
name: string;
|
|
49
58
|
emoji?: string;
|
|
@@ -59,7 +68,7 @@ export interface SaveMemberInput {
|
|
|
59
68
|
skills?: string[];
|
|
60
69
|
system?: string;
|
|
61
70
|
}
|
|
62
|
-
export declare function
|
|
71
|
+
export declare function saveAgent(input: SaveAgentInput): Promise<AgentDetail>;
|
|
63
72
|
/** Archive/unarchive a member: toggles `archived:` in agent.yml, nothing else. */
|
|
64
|
-
export declare function
|
|
65
|
-
export declare function
|
|
73
|
+
export declare function setAgentArchived(slug: string, archived: boolean): Promise<AgentDetail>;
|
|
74
|
+
export declare function deleteAgent(slug: string): Promise<void>;
|
|
@@ -3,24 +3,32 @@ import path from "node:path";
|
|
|
3
3
|
import { parse, stringify } from "yaml";
|
|
4
4
|
import { resolveProject } from "../config.js";
|
|
5
5
|
import { getProjectRoot } from "./context.js";
|
|
6
|
+
import { syncProjectAgents } from "./instances.js";
|
|
6
7
|
/**
|
|
7
|
-
*
|
|
8
|
+
* Agents: persistent agents, defined on the filesystem. Each one
|
|
8
9
|
* is one folder under the project's agents/ root:
|
|
9
10
|
*
|
|
10
11
|
* agents/<slug>/agent.yml # name, emoji, harness, model, effort, workflows
|
|
11
|
-
* agents/<slug>/system.md # the
|
|
12
|
+
* agents/<slug>/system.md # the agent's system prompt / role description
|
|
12
13
|
*
|
|
13
14
|
* Definitions are git-tracked project config (like workflows/), not run
|
|
14
15
|
* state — sessions with a member are ordinary chats scoped to it.
|
|
15
16
|
*/
|
|
16
17
|
export const HARNESSES = ["claude", "codex", "pi"];
|
|
17
18
|
export const EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
18
|
-
export
|
|
19
|
+
export const toSummary = (a) => ({
|
|
20
|
+
slug: a.slug,
|
|
21
|
+
name: a.name,
|
|
22
|
+
emoji: a.emoji,
|
|
23
|
+
...(a.description ? { description: a.description } : {}),
|
|
24
|
+
...(a.group ? { group: a.group } : {}),
|
|
25
|
+
});
|
|
26
|
+
export async function agentsRoot() {
|
|
19
27
|
const project = await resolveProject(getProjectRoot());
|
|
20
28
|
return path.resolve(project.root, project.config.agents ?? "agents");
|
|
21
29
|
}
|
|
22
30
|
const SLUG_RE = /^[a-z0-9][a-z0-9-]{0,48}$/;
|
|
23
|
-
export function
|
|
31
|
+
export function safeAgentSlug(slug) {
|
|
24
32
|
if (!SLUG_RE.test(slug))
|
|
25
33
|
throw new Error(`invalid agent slug "${slug}"`);
|
|
26
34
|
return slug;
|
|
@@ -55,8 +63,8 @@ function normalize(slug, raw) {
|
|
|
55
63
|
...(raw.archived === true ? { archived: true } : {}),
|
|
56
64
|
};
|
|
57
65
|
}
|
|
58
|
-
export async function
|
|
59
|
-
const root = await
|
|
66
|
+
export async function listAgents() {
|
|
67
|
+
const root = await agentsRoot();
|
|
60
68
|
let entries;
|
|
61
69
|
try {
|
|
62
70
|
entries = (await fs.readdir(root, { withFileTypes: true }))
|
|
@@ -66,7 +74,7 @@ export async function listMembers() {
|
|
|
66
74
|
catch {
|
|
67
75
|
return [];
|
|
68
76
|
}
|
|
69
|
-
const
|
|
77
|
+
const agents = await Promise.all(entries.map(async (slug) => {
|
|
70
78
|
try {
|
|
71
79
|
const raw = parse(await fs.readFile(path.join(root, slug, "agent.yml"), "utf8"));
|
|
72
80
|
return normalize(slug, (raw ?? {}));
|
|
@@ -75,10 +83,17 @@ export async function listMembers() {
|
|
|
75
83
|
return null;
|
|
76
84
|
}
|
|
77
85
|
}));
|
|
78
|
-
|
|
86
|
+
const list = agents.filter(Boolean).sort((a, b) => a.name.localeCompare(b.name));
|
|
87
|
+
// Keep the registry's copy of the roster current for the other instances'
|
|
88
|
+
// palettes. Awaited so a read that follows a save sees the record updated;
|
|
89
|
+
// it only writes when the roster changed.
|
|
90
|
+
await resolveProject(getProjectRoot())
|
|
91
|
+
.then((p) => syncProjectAgents(p.root, list.filter((a) => !a.archived).map(toSummary)))
|
|
92
|
+
.catch(() => { });
|
|
93
|
+
return list;
|
|
79
94
|
}
|
|
80
|
-
export async function
|
|
81
|
-
const dir = path.join(await
|
|
95
|
+
export async function getAgent(slug) {
|
|
96
|
+
const dir = path.join(await agentsRoot(), safeAgentSlug(slug));
|
|
82
97
|
let raw;
|
|
83
98
|
try {
|
|
84
99
|
raw = parse(await fs.readFile(path.join(dir, "agent.yml"), "utf8"));
|
|
@@ -89,7 +104,7 @@ export async function getMember(slug) {
|
|
|
89
104
|
const system = await fs.readFile(path.join(dir, "system.md"), "utf8").catch(() => "");
|
|
90
105
|
return { ...normalize(slug, (raw ?? {})), system: system.trim() };
|
|
91
106
|
}
|
|
92
|
-
export async function
|
|
107
|
+
export async function saveAgent(input) {
|
|
93
108
|
const name = input.name?.trim();
|
|
94
109
|
if (!name)
|
|
95
110
|
throw new Error("name is required");
|
|
@@ -99,8 +114,8 @@ export async function saveMember(input) {
|
|
|
99
114
|
if (input.effort && !EFFORTS.includes(input.effort)) {
|
|
100
115
|
throw new Error(`effort must be one of: ${EFFORTS.join(", ")}`);
|
|
101
116
|
}
|
|
102
|
-
const slug = input.slug ?
|
|
103
|
-
const dir = path.join(await
|
|
117
|
+
const slug = input.slug ? safeAgentSlug(input.slug) : slugFromName(name);
|
|
118
|
+
const dir = path.join(await agentsRoot(), slug);
|
|
104
119
|
await fs.mkdir(dir, { recursive: true });
|
|
105
120
|
// Profile edits must not silently unarchive: carry the flag over.
|
|
106
121
|
const existing = await fs
|
|
@@ -122,11 +137,11 @@ export async function saveMember(input) {
|
|
|
122
137
|
};
|
|
123
138
|
await fs.writeFile(path.join(dir, "agent.yml"), stringify(yml));
|
|
124
139
|
await fs.writeFile(path.join(dir, "system.md"), (input.system ?? "").trim() + "\n");
|
|
125
|
-
return (await
|
|
140
|
+
return (await getAgent(slug));
|
|
126
141
|
}
|
|
127
142
|
/** Archive/unarchive a member: toggles `archived:` in agent.yml, nothing else. */
|
|
128
|
-
export async function
|
|
129
|
-
const dir = path.join(await
|
|
143
|
+
export async function setAgentArchived(slug, archived) {
|
|
144
|
+
const dir = path.join(await agentsRoot(), safeAgentSlug(slug));
|
|
130
145
|
let raw;
|
|
131
146
|
try {
|
|
132
147
|
raw = (parse(await fs.readFile(path.join(dir, "agent.yml"), "utf8")) ?? {});
|
|
@@ -140,9 +155,9 @@ export async function setMemberArchived(slug, archived) {
|
|
|
140
155
|
else
|
|
141
156
|
delete yml.archived;
|
|
142
157
|
await fs.writeFile(path.join(dir, "agent.yml"), stringify(yml));
|
|
143
|
-
return (await
|
|
158
|
+
return (await getAgent(slug));
|
|
144
159
|
}
|
|
145
|
-
export async function
|
|
146
|
-
const dir = path.join(await
|
|
160
|
+
export async function deleteAgent(slug) {
|
|
161
|
+
const dir = path.join(await agentsRoot(), safeAgentSlug(slug));
|
|
147
162
|
await fs.rm(dir, { recursive: true, force: true });
|
|
148
163
|
}
|
|
@@ -19,7 +19,7 @@ export interface BackendHooks {
|
|
|
19
19
|
}>): Promise<string | null>;
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
|
-
* Optional per-chat model/effort overrides (from
|
|
22
|
+
* Optional per-chat model/effort overrides (from an agent definition).
|
|
23
23
|
* Each backend maps them onto whatever its agent actually supports.
|
|
24
24
|
*/
|
|
25
25
|
export interface BackendTuning {
|
|
@@ -5,7 +5,8 @@ import { knowledgeIndex } from "../knowledge.js";
|
|
|
5
5
|
import { getRun, listRuns, safeRunDir } from "../runs.js";
|
|
6
6
|
import { listSkills, readSkill } from "../skills.js";
|
|
7
7
|
import { listWorkflows } from "../workflows.js";
|
|
8
|
-
import {
|
|
8
|
+
import { getAgent, listAgents } from "../agents.js";
|
|
9
|
+
import { listRepos } from "../repos.js";
|
|
9
10
|
import { startAcpBackend } from "./acp.js";
|
|
10
11
|
import { startPiBackend } from "./pi.js";
|
|
11
12
|
import { appendEvent, listChatMetas, newChatId, readEvents, readMeta, safeChatDir, writeMeta, } from "./store.js";
|
|
@@ -74,19 +75,19 @@ function emit(state, ev) {
|
|
|
74
75
|
/* ---------- skills ---------- */
|
|
75
76
|
/**
|
|
76
77
|
* Skills visible to a chat: every discovered skill (workspace skills root
|
|
77
|
-
* + .claude/skills roots, plus the
|
|
78
|
-
* sessions), narrowed by the
|
|
78
|
+
* + .claude/skills roots, plus the agent's own agents/<slug>/skills for agent
|
|
79
|
+
* sessions), narrowed by the agent's allowlist when the chat is an agent
|
|
79
80
|
* session (absent allowlist = all, empty = none). The agent's own skills
|
|
80
81
|
* always apply — the allowlist narrows shared skills only.
|
|
81
82
|
*/
|
|
82
83
|
async function availableSkills(scope) {
|
|
83
|
-
const all = await listSkills(scope.kind === "
|
|
84
|
-
if (scope.kind !== "
|
|
84
|
+
const all = await listSkills(scope.kind === "agent" ? scope.slug : undefined).catch(() => []);
|
|
85
|
+
if (scope.kind !== "agent")
|
|
85
86
|
return all;
|
|
86
|
-
const
|
|
87
|
-
if (!
|
|
87
|
+
const def = await getAgent(scope.slug).catch(() => null);
|
|
88
|
+
if (!def || def.skills === undefined)
|
|
88
89
|
return all;
|
|
89
|
-
const allowed = new Set(
|
|
90
|
+
const allowed = new Set(def.skills.map((n) => n.toLowerCase()));
|
|
90
91
|
return all.filter((s) => s.source === "agent" || allowed.has(s.name.toLowerCase()));
|
|
91
92
|
}
|
|
92
93
|
/** "## Your skills" context block (empty when no skills are visible). */
|
|
@@ -122,12 +123,12 @@ async function expandSkillInvocation(scope, text) {
|
|
|
122
123
|
`Arguments the user passed to the skill: ${args || "(none)"}`);
|
|
123
124
|
}
|
|
124
125
|
/* ---------- scope context ---------- */
|
|
125
|
-
/** "## Your knowledge" block for a
|
|
126
|
-
async function
|
|
127
|
-
if (
|
|
126
|
+
/** "## Your knowledge" block for a agent's connected OKF bundles. */
|
|
127
|
+
async function agentKnowledgeContext(def) {
|
|
128
|
+
if (def.knowledge.length === 0)
|
|
128
129
|
return "";
|
|
129
130
|
const { bundles } = await knowledgeIndex().catch(() => ({ bundles: [] }));
|
|
130
|
-
const lines =
|
|
131
|
+
const lines = def.knowledge
|
|
131
132
|
.map((name) => {
|
|
132
133
|
const b = bundles.find((x) => x.name === name);
|
|
133
134
|
return b
|
|
@@ -139,30 +140,53 @@ async function teamKnowledgeContext(member) {
|
|
|
139
140
|
`part of your job. Consult them before answering questions in their domain, and keep them current ` +
|
|
140
141
|
`when you learn something durable:\n${lines}\n\n` +
|
|
141
142
|
`Read with \`npx kraftwerk knowledge list <bundle>\`, \`get <bundle>/<path>\`, \`search <text>\`. ` +
|
|
142
|
-
`Write ONLY through \`npx kraftwerk knowledge put <bundle>/<path> --file <tmp.md> --actor ${
|
|
143
|
+
`Write ONLY through \`npx kraftwerk knowledge put <bundle>/<path> --file <tmp.md> --actor ${def.slug}/${def.harness}\` ` +
|
|
143
144
|
`(write the markdown to a temp file first; the CLI stamps provenance and maintains index.md/log.md). ` +
|
|
144
145
|
`Never hand-edit index.md or log.md, and never add \`verified\` yourself — verification is the human's ` +
|
|
145
146
|
`click in the UI.\n\n`);
|
|
146
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* "## Repositories" block: the clones under the repos root, when the
|
|
150
|
+
* feature is on. Empty when it is off, so agents in a workspace without
|
|
151
|
+
* it never hear about it.
|
|
152
|
+
*/
|
|
153
|
+
async function reposContext() {
|
|
154
|
+
const view = await listRepos().catch(() => null);
|
|
155
|
+
if (!view?.enabled || !view.root)
|
|
156
|
+
return "";
|
|
157
|
+
const lines = view.repos
|
|
158
|
+
.map((r) => {
|
|
159
|
+
const state = r.error ? `unreadable: ${r.error}` : [r.dirty ? `${r.dirty} uncommitted` : "clean", r.ahead ? `${r.ahead} ahead` : "", r.behind ? `${r.behind} behind` : ""].filter(Boolean).join(", ");
|
|
160
|
+
return `- ${r.slug} — ${r.path}${r.url ? ` (${r.url})` : ""}${r.branch ? `, branch ${r.branch}` : ""}${r.head ? ` @ ${r.head}` : ""}, ${state}`;
|
|
161
|
+
})
|
|
162
|
+
.join("\n");
|
|
163
|
+
return (`## Repositories\nGit repositories this workspace works on live under ${view.root} (one clone per folder). ` +
|
|
164
|
+
`When the user names one of them, work inside its folder: read its README and structure first, keep commits on a ` +
|
|
165
|
+
`branch unless told otherwise, and never push or force-push without asking.\n${lines || "(none cloned yet)"}\n\n` +
|
|
166
|
+
`Clone a new one with \`npx kraftwerk repos add <url> [--name <folder>] [--branch <b>] [--depth <n>]\` so it lands in ` +
|
|
167
|
+
`that root (\`--depth 1\` for a large repository; a plain \`git clone\` into that folder works too); ` +
|
|
168
|
+
`\`npx kraftwerk repos\` lists them, \`update <name>\` fetches and fast-forwards a clean clone, \`remove <name>\` ` +
|
|
169
|
+
`deletes one. A repository the user mentions that is not listed is not cloned yet — offer to add it.`);
|
|
170
|
+
}
|
|
147
171
|
/** Base context per scope; scopeContext() appends the shared skills block. */
|
|
148
172
|
async function baseScopeContext(scope, agent) {
|
|
149
|
-
if (scope.kind === "
|
|
150
|
-
const
|
|
151
|
-
if (!
|
|
152
|
-
return `You were opened as
|
|
173
|
+
if (scope.kind === "agent") {
|
|
174
|
+
const def = await getAgent(scope.slug).catch(() => null);
|
|
175
|
+
if (!def) {
|
|
176
|
+
return `You were opened as agent "${scope.slug}", but its definition under agents/ is missing. Tell the user and ask them to recreate it.`;
|
|
153
177
|
}
|
|
154
178
|
const { workflows } = await listWorkflows().catch(() => ({ workflows: [] }));
|
|
155
|
-
const connected = workflows.filter((w) =>
|
|
156
|
-
const missing =
|
|
179
|
+
const connected = workflows.filter((w) => def.workflows.includes(w.slug));
|
|
180
|
+
const missing = def.workflows.filter((slug) => !connected.some((w) => w.slug === slug));
|
|
157
181
|
const wfLines = connected
|
|
158
182
|
.map((w) => `- ${w.slug}: ${w.description ?? w.name ?? ""} (${w.steps} steps)`)
|
|
159
183
|
.join("\n");
|
|
160
|
-
return (`You are ${
|
|
161
|
-
`(defined in agents/${
|
|
162
|
-
`is a conversation with the same ${
|
|
184
|
+
return (`You are ${def.emoji} ${def.name}, a persistent agent of this project ` +
|
|
185
|
+
`(defined in agents/${def.slug}/). The user works with you like with a colleague: every session ` +
|
|
186
|
+
`is a conversation with the same ${def.name}, so keep this role consistently. You are an AI agent, ` +
|
|
163
187
|
`not a human — never pretend otherwise, but do own your role.\n\n` +
|
|
164
|
-
`## Your role\n${
|
|
165
|
-
(
|
|
188
|
+
`## Your role\n${def.system || "(no system prompt written yet — ask the user what your job should be)"}\n\n` +
|
|
189
|
+
(def.workflows.length > 0
|
|
166
190
|
? `## Your workflows\nThese kraftwerk workflows are part of your job. When the user's request matches one, ` +
|
|
167
191
|
`run it yourself instead of doing the work by hand:\n${wfLines || "(none found)"}\n` +
|
|
168
192
|
(missing.length > 0
|
|
@@ -174,9 +198,9 @@ async function baseScopeContext(scope, agent) {
|
|
|
174
198
|
`run id and summarize the result. Confirm with the user before starting a long or expensive run ` +
|
|
175
199
|
`unless they clearly asked for it.\n\n`
|
|
176
200
|
: "") +
|
|
177
|
-
(await
|
|
201
|
+
(await agentKnowledgeContext(def)) +
|
|
178
202
|
`The working directory is the project root. Stay within your role; if a request is clearly outside it, ` +
|
|
179
|
-
`say so and suggest which
|
|
203
|
+
`say so and suggest which agent or tool fits better.` +
|
|
180
204
|
(scope.routine
|
|
181
205
|
? `\n\nThis session was started automatically by your scheduled routine "${scope.routine}" — ` +
|
|
182
206
|
`nobody may be watching live. Complete the task autonomously (tool permissions are ` +
|
|
@@ -212,11 +236,11 @@ async function baseScopeContext(scope, agent) {
|
|
|
212
236
|
`verification is the human's click in the UI. Ask the user what knowledge to capture, then author concise, well-typed concepts.`);
|
|
213
237
|
}
|
|
214
238
|
if (scope.kind === "kraftwerk") {
|
|
215
|
-
const [{ workflows }, runs, knowledge,
|
|
239
|
+
const [{ workflows }, runs, knowledge, agents] = await Promise.all([
|
|
216
240
|
listWorkflows(),
|
|
217
241
|
listRuns(),
|
|
218
242
|
knowledgeIndex().catch(() => ({ bundles: [] })),
|
|
219
|
-
|
|
243
|
+
listAgents().catch(() => []),
|
|
220
244
|
]);
|
|
221
245
|
const wfLines = workflows
|
|
222
246
|
.map((w) => `- ${w.slug}: ${w.description ?? w.name ?? ""} (${w.steps} steps, ${w.agents} agents)`)
|
|
@@ -228,7 +252,7 @@ async function baseScopeContext(scope, agent) {
|
|
|
228
252
|
const bundleLines = knowledge.bundles
|
|
229
253
|
.map((b) => `- ${b.name} (${b.concepts} concepts${b.updatedAt ? `, updated ${b.updatedAt.slice(0, 10)}` : ""})`)
|
|
230
254
|
.join("\n");
|
|
231
|
-
const
|
|
255
|
+
const agentLines = agents
|
|
232
256
|
.map((m) => `- ${m.emoji} ${m.name} (${m.slug})${m.description ? `: ${m.description}` : ""}`)
|
|
233
257
|
.join("\n");
|
|
234
258
|
return (`You are the assistant inside the kraftwerk inspector, a UI for a workflow-as-code agent framework. ` +
|
|
@@ -244,8 +268,8 @@ async function baseScopeContext(scope, agent) {
|
|
|
244
268
|
`Read with \`npx kraftwerk knowledge list [bundle]\`, \`get <bundle>/<path>\`, \`search <text>\`. ` +
|
|
245
269
|
`Write ONLY through \`npx kraftwerk knowledge put <bundle>/<path> --file <tmp.md> --actor kraftwerk-chat/${agent}\` ` +
|
|
246
270
|
`(stamps provenance, maintains index.md/log.md — never hand-edit those).\n\n` +
|
|
247
|
-
`##
|
|
248
|
-
`These are persistent
|
|
271
|
+
`## Agents\n${agentLines || "(none)"}\n` +
|
|
272
|
+
`These are persistent agents (defined under agents/); the user talks to them on the ` +
|
|
249
273
|
`Agents screen. Point the user there when a request clearly belongs to one of them.\n\n` +
|
|
250
274
|
`Answer questions about workflows and runs by reading the files above. Do not modify run outputs unless asked.`);
|
|
251
275
|
}
|
|
@@ -278,25 +302,28 @@ const RENDERING_BLOCK = `## Chat rendering\n` +
|
|
|
278
302
|
`Prefer these relative /api/... URLs over file paths when showing results: they render directly ` +
|
|
279
303
|
`in this chat and keep working wherever the inspector is reachable.`;
|
|
280
304
|
async function scopeContext(scope, agent) {
|
|
281
|
-
|
|
305
|
+
// The repositories block reads every clone from git, so it runs alongside
|
|
306
|
+
// the rest instead of adding its spawns to the first prompt's latency.
|
|
307
|
+
const [base, repos, skills] = await Promise.all([
|
|
282
308
|
baseScopeContext(scope, agent),
|
|
309
|
+
scope.kind === "agent" || scope.kind === "kraftwerk" ? reposContext() : Promise.resolve(""),
|
|
283
310
|
availableSkills(scope),
|
|
284
311
|
]);
|
|
285
|
-
return [base, RENDERING_BLOCK, skillsBlock(skills)].filter(Boolean).join("\n\n");
|
|
312
|
+
return [base, repos, RENDERING_BLOCK, skillsBlock(skills)].filter(Boolean).join("\n\n");
|
|
286
313
|
}
|
|
287
314
|
/* ---------- backend lifecycle ---------- */
|
|
288
|
-
/**
|
|
315
|
+
/** Agent sessions carry the agent's model/effort; resolved live so edits apply to new backends. */
|
|
289
316
|
async function backendTuning(scope, agent) {
|
|
290
317
|
const tuning = {};
|
|
291
|
-
if (scope.kind === "
|
|
292
|
-
const
|
|
293
|
-
if (
|
|
294
|
-
tuning.model =
|
|
295
|
-
if (
|
|
296
|
-
tuning.effort =
|
|
297
|
-
// Claude discovers skills natively;
|
|
298
|
-
if (agent === "claude" &&
|
|
299
|
-
tuning.skills =
|
|
318
|
+
if (scope.kind === "agent") {
|
|
319
|
+
const def = await getAgent(scope.slug).catch(() => null);
|
|
320
|
+
if (def?.model)
|
|
321
|
+
tuning.model = def.model;
|
|
322
|
+
if (def?.effort)
|
|
323
|
+
tuning.effort = def.effort;
|
|
324
|
+
// Claude discovers skills natively; an agentined allowlist narrows that.
|
|
325
|
+
if (agent === "claude" && def?.skills)
|
|
326
|
+
tuning.skills = def.skills;
|
|
300
327
|
}
|
|
301
328
|
// Run chats live in the run folder — grant claude the project root so
|
|
302
329
|
// project-level skills and files stay reachable.
|
|
@@ -319,7 +346,7 @@ async function ensureBackend(state) {
|
|
|
319
346
|
// Routine-triggered sessions run unattended: auto-approve, but keep
|
|
320
347
|
// the request/resolution pair in the thread as an audit trail.
|
|
321
348
|
const scope = state.meta.scope;
|
|
322
|
-
if (scope.kind === "
|
|
349
|
+
if (scope.kind === "agent" && scope.routine) {
|
|
323
350
|
const allow = options.find((o) => o.kind?.startsWith("allow")) ?? options[0];
|
|
324
351
|
const requestId = randomUUID();
|
|
325
352
|
emit(state, { type: "permission_request", requestId, title, options });
|
|
@@ -418,7 +445,7 @@ export async function postMessage(id, text) {
|
|
|
418
445
|
// Routine sessions are one-shot and unattended: release the agent
|
|
419
446
|
// process as soon as the turn ends instead of waiting for the reaper.
|
|
420
447
|
const scope = state.meta.scope;
|
|
421
|
-
if (scope.kind === "
|
|
448
|
+
if (scope.kind === "agent" && scope.routine)
|
|
422
449
|
dropBackend(state);
|
|
423
450
|
}
|
|
424
451
|
catch (err) {
|
|
@@ -26,12 +26,24 @@ export async function writeMeta(meta) {
|
|
|
26
26
|
}
|
|
27
27
|
export async function readMeta(id) {
|
|
28
28
|
try {
|
|
29
|
-
return JSON.parse(await fs.readFile(path.join(safeChatDir(id), "meta.json"), "utf8"));
|
|
29
|
+
return upgradeMeta(JSON.parse(await fs.readFile(path.join(safeChatDir(id), "meta.json"), "utf8")));
|
|
30
30
|
}
|
|
31
31
|
catch {
|
|
32
32
|
return null;
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Chats written before 0.36 scoped agent sessions as
|
|
37
|
+
* { kind: "team", member } — read them as { kind: "agent", slug } so the
|
|
38
|
+
* folders on disk never need rewriting.
|
|
39
|
+
*/
|
|
40
|
+
function upgradeMeta(meta) {
|
|
41
|
+
const legacy = meta.scope;
|
|
42
|
+
if (legacy?.kind === "team" && typeof legacy.member === "string") {
|
|
43
|
+
meta.scope = { kind: "agent", slug: legacy.member, ...(legacy.routine ? { routine: legacy.routine } : {}) };
|
|
44
|
+
}
|
|
45
|
+
return meta;
|
|
46
|
+
}
|
|
35
47
|
export async function appendEvent(id, ev) {
|
|
36
48
|
await fs.appendFile(path.join(safeChatDir(id), "events.jsonl"), JSON.stringify(ev) + "\n");
|
|
37
49
|
}
|
package/dist/inspector/git.d.ts
CHANGED
|
@@ -40,6 +40,20 @@ export interface GitDiff {
|
|
|
40
40
|
/** The diff was cut at MAX_DIFF; the rest is available in a terminal. */
|
|
41
41
|
truncated?: boolean;
|
|
42
42
|
}
|
|
43
|
+
export interface GitResult {
|
|
44
|
+
ok: boolean;
|
|
45
|
+
/** Exit status, or null when git never ran (spawn error) or was killed. */
|
|
46
|
+
code: number | null;
|
|
47
|
+
stdout: string;
|
|
48
|
+
stderr: string;
|
|
49
|
+
}
|
|
50
|
+
export declare const NET_TIMEOUT = 60000;
|
|
51
|
+
export declare function git(args: string[], cwd: string, timeoutMs?: number, env?: NodeJS.ProcessEnv): Promise<GitResult>;
|
|
52
|
+
export declare function sshCommandFor(repoRoot: string): Promise<string>;
|
|
53
|
+
/** A git call that may touch the network: longer timeout, non-interactive ssh. */
|
|
54
|
+
export declare function gitNet(args: string[], repoRoot: string): Promise<GitResult>;
|
|
55
|
+
/** Repo root for a directory, or null when it is not inside a git repo. */
|
|
56
|
+
export declare function repoRootFor(dir: string): Promise<string | null>;
|
|
43
57
|
/**
|
|
44
58
|
* Everything the git screen renders. Each call shells out to git a handful
|
|
45
59
|
* of times, and the nav badge polls it from every open tab, so results are
|