@netnodeag/kraftwerk 0.75.0 → 0.76.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/dist/cli/daemon-control.js +32 -13
- package/dist/cli/daemon-login.js +3 -3
- package/dist/cli/daemon.js +5 -4
- package/dist/cli/doctor.d.ts +3 -1
- package/dist/cli/doctor.js +40 -5
- package/dist/cli/init.js +5 -2
- package/dist/cli/kraftwerk.js +9 -5
- package/dist/cli/routines.d.ts +2 -1
- package/dist/cli/routines.js +40 -4
- package/dist/cli/whoami.d.ts +9 -0
- package/dist/cli/whoami.js +48 -0
- package/dist/client/routes.d.ts +3 -0
- package/dist/client/routes.js +3 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.js +38 -3
- package/dist/core/attention.d.ts +8 -1
- package/dist/core/attention.js +38 -2
- package/dist/core/daemon.d.ts +15 -1
- package/dist/core/daemon.js +46 -8
- package/dist/core/identity.d.ts +46 -0
- package/dist/core/identity.js +106 -0
- package/dist/core/routines.d.ts +81 -0
- package/dist/core/routines.js +174 -33
- package/dist/server/api/agents.d.ts +5 -1
- package/dist/server/api/agents.js +19 -2
- package/dist/server/api/index.d.ts +14 -3
- package/dist/server/api/runs.d.ts +3 -1
- package/dist/server/api/runs.js +9 -1
- package/dist/server/api/workspace.d.ts +6 -1
- package/dist/server/api/workspace.js +5 -0
- package/dist/server/push.js +1 -1
- package/package.json +1 -1
- package/schema/kraftwerk.schema.json +5 -0
- package/web/dist/assets/{dist-YNCNGGgK.js → dist-BQpWYkxm.js} +1 -1
- package/web/dist/assets/{dist-DaGnyH_l.js → dist-BTVP-6On.js} +1 -1
- package/web/dist/assets/dist-BURf1l-A.js +1 -0
- package/web/dist/assets/{dist-CLZjnhZZ.js → dist-BYkcY-Vl.js} +1 -1
- package/web/dist/assets/{dist-CzM6tn0f.js → dist-BwIca_Uj.js} +1 -1
- package/web/dist/assets/{dist-Y9aHncjU.js → dist-DFXmaDc5.js} +1 -1
- package/web/dist/assets/{dist-CezfArgg.js → dist-T1sNLC62.js} +1 -1
- package/web/dist/assets/{dist-CYicby0c.js → dist-fgB7OEJy.js} +1 -1
- package/web/dist/assets/{dist-QqFmuPda.js → dist-flnLXEzU.js} +1 -1
- package/web/dist/assets/{dist-BI4umsk6.js → dist-l7foWlFx.js} +1 -1
- package/web/dist/assets/{dist-CODxhtAK.js → dist-vuciRCSz.js} +1 -1
- package/web/dist/assets/{editor-DEb8z5-Q.js → editor-C6Ok8NPo.js} +3 -3
- package/web/dist/assets/index-z4tpCCzk.js +231 -0
- package/web/dist/index.html +1 -1
- package/web/dist/assets/dist-DtktO5hc.js +0 -1
- package/web/dist/assets/index-kmyK4tFm.js +0 -231
package/dist/core/routines.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { promises as fs } from "node:fs";
|
|
1
|
+
import { promises as fs, realpathSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { parse, stringify } from "yaml";
|
|
4
|
-
import { getOutputDir } from "./context.js";
|
|
4
|
+
import { getOutputDir, workspaceRoot } from "./context.js";
|
|
5
5
|
import { perWorkspace } from "./workspace.js";
|
|
6
6
|
import { getAgent, listAgents, safeAgentSlug, agentsRoot } from "./agents.js";
|
|
7
7
|
import { chatAwaitingApproval, createChat, postMessage } from "./chat/sessions.js";
|
|
8
8
|
import { pushNotification } from "./notifications.js";
|
|
9
|
+
import { isIdentityString, localIdentity, readIdentity, identityString } from "./identity.js";
|
|
10
|
+
import { gitCommit, repoRootFor } from "./git.js";
|
|
9
11
|
/* ---------- cron ---------- */
|
|
10
12
|
const ALIASES = {
|
|
11
13
|
"@hourly": "0 * * * *",
|
|
@@ -100,8 +102,26 @@ function normalizeRoutine(raw) {
|
|
|
100
102
|
schedule: String(raw.schedule ?? ""),
|
|
101
103
|
prompt: String(raw.prompt ?? ""),
|
|
102
104
|
enabled: raw.enabled !== false,
|
|
105
|
+
...(typeof raw.runs_on === "string" && raw.runs_on.trim() ? { runsOn: raw.runs_on.trim() } : {}),
|
|
106
|
+
...(typeof raw.private === "boolean" ? { private: raw.private } : {}),
|
|
103
107
|
};
|
|
104
108
|
}
|
|
109
|
+
/** The file's own spelling: snake_case `runs_on`, optional keys only when set. */
|
|
110
|
+
function serializeRoutine(r) {
|
|
111
|
+
return {
|
|
112
|
+
id: r.id,
|
|
113
|
+
name: r.name,
|
|
114
|
+
schedule: r.schedule,
|
|
115
|
+
prompt: r.prompt,
|
|
116
|
+
enabled: r.enabled,
|
|
117
|
+
...(r.runsOn ? { runs_on: r.runsOn } : {}),
|
|
118
|
+
...(r.private !== undefined ? { private: r.private } : {}),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/** Why a runs_on value is refused, or undefined. */
|
|
122
|
+
function runsOnError(value) {
|
|
123
|
+
return isIdentityString(value) ? undefined : `runs_on must be <handle>@<machine> (lowercase letters, digits, ".", "_", "-"), got "${value}"`;
|
|
124
|
+
}
|
|
105
125
|
export async function listRoutines(slug) {
|
|
106
126
|
let raw;
|
|
107
127
|
try {
|
|
@@ -117,7 +137,7 @@ export async function listRoutines(slug) {
|
|
|
117
137
|
.filter(Boolean);
|
|
118
138
|
}
|
|
119
139
|
async function writeRoutines(slug, routines) {
|
|
120
|
-
await fs.writeFile(await routinesFile(slug), stringify(routines));
|
|
140
|
+
await fs.writeFile(await routinesFile(slug), stringify(routines.map(serializeRoutine)));
|
|
121
141
|
}
|
|
122
142
|
export async function saveRoutine(slug, input) {
|
|
123
143
|
const name = input.name?.trim();
|
|
@@ -134,15 +154,26 @@ export async function saveRoutine(slug, input) {
|
|
|
134
154
|
.slice(0, 48);
|
|
135
155
|
if (!id)
|
|
136
156
|
throw new Error("name must contain at least one letter or digit");
|
|
157
|
+
const given = typeof input.runsOn === "string" ? input.runsOn.trim() : input.runsOn;
|
|
158
|
+
if (given) {
|
|
159
|
+
const err = runsOnError(given);
|
|
160
|
+
if (err)
|
|
161
|
+
throw new Error(err);
|
|
162
|
+
}
|
|
163
|
+
const routines = await listRoutines(slug);
|
|
164
|
+
const i = routines.findIndex((r) => r.id === id);
|
|
165
|
+
const existing = i >= 0 ? routines[i] : undefined;
|
|
166
|
+
const runsOn = given === undefined ? (existing ? existing.runsOn : await localIdentity()) : given || undefined;
|
|
167
|
+
const priv = input.private ?? existing?.private;
|
|
137
168
|
const routine = {
|
|
138
169
|
id,
|
|
139
170
|
name,
|
|
140
171
|
schedule: input.schedule.trim(),
|
|
141
172
|
prompt: input.prompt,
|
|
142
173
|
enabled: input.enabled !== false,
|
|
174
|
+
...(runsOn ? { runsOn } : {}),
|
|
175
|
+
...(priv !== undefined ? { private: priv } : {}),
|
|
143
176
|
};
|
|
144
|
-
const routines = await listRoutines(slug);
|
|
145
|
-
const i = routines.findIndex((r) => r.id === id);
|
|
146
177
|
if (i >= 0)
|
|
147
178
|
routines[i] = routine;
|
|
148
179
|
else
|
|
@@ -191,13 +222,19 @@ export async function allRoutines(workflows) {
|
|
|
191
222
|
}))));
|
|
192
223
|
return per.flat();
|
|
193
224
|
}
|
|
225
|
+
/** Whether a routine fires on the machine whose identity is `me`: enabled and runs_on exactly that. */
|
|
226
|
+
export const firesHere = (r, me) => r.enabled && r.runsOn === me;
|
|
194
227
|
export async function routineStatuses(slug) {
|
|
195
|
-
const [routines, state] = await Promise.all([listRoutines(slug), readState()]);
|
|
228
|
+
const [routines, state, id] = await Promise.all([listRoutines(slug), readState(), readIdentity()]);
|
|
229
|
+
const me = identityString(id);
|
|
196
230
|
return routines.map((r) => {
|
|
197
231
|
const s = state[`${slug}/${r.id}`] ?? {};
|
|
232
|
+
const runner = !r.runsOn ? "unassigned" : r.runsOn === me ? "here" : "elsewhere";
|
|
198
233
|
return {
|
|
199
234
|
...r,
|
|
200
|
-
|
|
235
|
+
runner,
|
|
236
|
+
...(r.runsOn && id.previous.includes(r.runsOn) ? { retired: true } : {}),
|
|
237
|
+
...(firesHere(r, me) ? { nextRunAt: nextRun(r.schedule) } : {}),
|
|
201
238
|
...(s.lastRunAt ? { lastRunAt: s.lastRunAt } : {}),
|
|
202
239
|
...(s.lastChatId ? { lastChatId: s.lastChatId } : {}),
|
|
203
240
|
...(s.lastChatId && chatAwaitingApproval(s.lastChatId) ? { awaitingApproval: true } : {}),
|
|
@@ -254,48 +291,152 @@ export async function runRoutineNow(slug, id) {
|
|
|
254
291
|
/* ---------- scheduler ---------- */
|
|
255
292
|
/** The scheduler's timer, one per workspace. */
|
|
256
293
|
const scheduler = perWorkspace(() => ({ timer: null }));
|
|
294
|
+
/**
|
|
295
|
+
* The routines due at `now` that fire on this machine: enabled, runs_on
|
|
296
|
+
* exactly this identity, cron matching. Everything else — unassigned,
|
|
297
|
+
* another machine's, a retired identity's — is never due here.
|
|
298
|
+
*/
|
|
299
|
+
export async function dueRoutines(now, me) {
|
|
300
|
+
const identity = me ?? (await localIdentity());
|
|
301
|
+
const out = [];
|
|
302
|
+
for (const m of await listAgents()) {
|
|
303
|
+
for (const r of await listRoutines(m.slug).catch(() => [])) {
|
|
304
|
+
if (!firesHere(r, identity))
|
|
305
|
+
continue;
|
|
306
|
+
try {
|
|
307
|
+
if (cronMatches(parseCron(r.schedule), now))
|
|
308
|
+
out.push({ slug: m.slug, routine: r });
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
// invalid schedule — surfaced by validation on save, skip here
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return out;
|
|
316
|
+
}
|
|
257
317
|
async function tick() {
|
|
258
318
|
const now = new Date();
|
|
259
319
|
const p = (n) => String(n).padStart(2, "0");
|
|
260
320
|
const minuteKey = `${now.getFullYear()}-${p(now.getMonth() + 1)}-${p(now.getDate())}T${p(now.getHours())}:${p(now.getMinutes())}`;
|
|
261
|
-
let
|
|
321
|
+
let due;
|
|
262
322
|
try {
|
|
263
|
-
|
|
323
|
+
due = await dueRoutines(now);
|
|
264
324
|
}
|
|
265
325
|
catch {
|
|
266
326
|
return;
|
|
267
327
|
}
|
|
328
|
+
if (!due.length)
|
|
329
|
+
return;
|
|
268
330
|
const state = await readState();
|
|
269
331
|
let dirty = false;
|
|
270
|
-
for (const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
332
|
+
for (const { slug, routine: r } of due) {
|
|
333
|
+
const key = `${slug}/${r.id}`;
|
|
334
|
+
if (state[key]?.lastMinute === minuteKey)
|
|
335
|
+
continue;
|
|
336
|
+
state[key] = { ...state[key], lastMinute: minuteKey };
|
|
337
|
+
dirty = true;
|
|
338
|
+
try {
|
|
339
|
+
const chatId = await fireRoutine(slug, r);
|
|
340
|
+
state[key] = { ...state[key], lastRunAt: now.toISOString(), lastChatId: chatId };
|
|
341
|
+
delete state[key].lastError;
|
|
342
|
+
}
|
|
343
|
+
catch (err) {
|
|
344
|
+
state[key] = { ...state[key], lastError: err.message };
|
|
345
|
+
void notifyRoutineFailed(slug, r, err.message);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (dirty)
|
|
349
|
+
await writeState(state);
|
|
350
|
+
}
|
|
351
|
+
/** Every routine in the workspace that has no runner, or names a retired identity of this machine. */
|
|
352
|
+
export async function runnerFindings() {
|
|
353
|
+
const id = await readIdentity();
|
|
354
|
+
const out = { unassigned: [], retired: [] };
|
|
355
|
+
let since = 0;
|
|
356
|
+
// Archived agents included: the scheduler fires their routines too.
|
|
357
|
+
for (const a of await listAgents()) {
|
|
358
|
+
const routines = await listRoutines(a.slug).catch(() => []);
|
|
359
|
+
let hit = false;
|
|
360
|
+
for (const r of routines) {
|
|
361
|
+
if (!r.runsOn && r.enabled) {
|
|
362
|
+
out.unassigned.push({ agent: a.slug, id: r.id, name: r.name });
|
|
363
|
+
hit = true;
|
|
277
364
|
}
|
|
278
|
-
|
|
279
|
-
|
|
365
|
+
else if (r.runsOn && id.previous.includes(r.runsOn)) {
|
|
366
|
+
out.retired.push({ agent: a.slug, id: r.id, name: r.name, from: r.runsOn });
|
|
367
|
+
hit = true;
|
|
280
368
|
}
|
|
281
|
-
|
|
282
|
-
|
|
369
|
+
}
|
|
370
|
+
if (hit) {
|
|
371
|
+
const st = await fs.stat(await routinesFile(a.slug)).catch(() => null);
|
|
372
|
+
if (st && st.mtimeMs > since)
|
|
373
|
+
since = st.mtimeMs;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (since)
|
|
377
|
+
out.since = new Date(since).toISOString();
|
|
378
|
+
return out;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* "Run these here": make this machine the runner of the routines `pick`
|
|
382
|
+
* selects, write routines.yml and commit just the files it touched — the
|
|
383
|
+
* commit is what tells the other clones who runs them. A workspace without
|
|
384
|
+
* git sync, or a commit that fails, keeps the written files and says why.
|
|
385
|
+
*/
|
|
386
|
+
export async function adoptRoutines(pick, message) {
|
|
387
|
+
const id = await readIdentity();
|
|
388
|
+
const me = identityString(id);
|
|
389
|
+
const adopted = [];
|
|
390
|
+
const written = [];
|
|
391
|
+
for (const a of await listAgents()) {
|
|
392
|
+
const routines = await listRoutines(a.slug).catch(() => []);
|
|
393
|
+
let changed = false;
|
|
394
|
+
for (const r of routines) {
|
|
395
|
+
if (r.runsOn === me || !pick(a.slug, r, id.previous))
|
|
283
396
|
continue;
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
catch (err) {
|
|
292
|
-
state[key] = { ...state[key], lastError: err.message };
|
|
293
|
-
void notifyRoutineFailed(m.slug, r, err.message);
|
|
294
|
-
}
|
|
397
|
+
adopted.push({ agent: a.slug, id: r.id, name: r.name, ...(r.runsOn ? { from: r.runsOn } : {}) });
|
|
398
|
+
r.runsOn = me;
|
|
399
|
+
changed = true;
|
|
400
|
+
}
|
|
401
|
+
if (changed) {
|
|
402
|
+
await writeRoutines(a.slug, routines);
|
|
403
|
+
written.push(await routinesFile(a.slug));
|
|
295
404
|
}
|
|
296
405
|
}
|
|
297
|
-
|
|
298
|
-
|
|
406
|
+
const root = workspaceRoot();
|
|
407
|
+
const files = written.map((f) => path.relative(root, f).split(path.sep).join("/"));
|
|
408
|
+
if (!written.length)
|
|
409
|
+
return { runsOn: me, adopted, files, commit: null };
|
|
410
|
+
return { runsOn: me, adopted, files, commit: await commitFiles(written, message ?? `Routines run on ${me}: ${adopted.map((x) => `${x.agent}/${x.id}`).join(", ")}`) };
|
|
411
|
+
}
|
|
412
|
+
async function commitFiles(abs, message) {
|
|
413
|
+
const repoRoot = await repoRootFor(path.dirname(abs[0]));
|
|
414
|
+
if (!repoRoot)
|
|
415
|
+
return { ok: false, error: "the workspace is not a git repository" };
|
|
416
|
+
// rev-parse answers with symlinks resolved (/var → /private/var on macOS); the files must be spelled the same way.
|
|
417
|
+
const real = (p) => {
|
|
418
|
+
try {
|
|
419
|
+
return realpathSync.native(p);
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
return path.resolve(p);
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
const rel = abs.map((f) => path.relative(repoRoot, real(f)).split(path.sep).join("/"));
|
|
426
|
+
return gitCommit(rel, message);
|
|
427
|
+
}
|
|
428
|
+
/** Adopt one routine (whatever its runs_on said). */
|
|
429
|
+
export async function adoptRoutine(slug, id) {
|
|
430
|
+
const safe = safeAgentSlug(slug);
|
|
431
|
+
if (!(await listRoutines(safe)).some((r) => r.id === id))
|
|
432
|
+
throw new Error("routine not found");
|
|
433
|
+
const result = await adoptRoutines((agent, r) => agent === safe && r.id === id);
|
|
434
|
+
const routine = (await routineStatuses(safe)).find((r) => r.id === id);
|
|
435
|
+
return { ...result, routine };
|
|
436
|
+
}
|
|
437
|
+
/** Adopt every enabled routine without runs_on — and, with `retired`, every one naming a retired identity of this machine. */
|
|
438
|
+
export function adoptUnassigned(opts = {}) {
|
|
439
|
+
return adoptRoutines((_agent, r, previous) => (!r.runsOn && r.enabled) || (!!opts.retired && !!r.runsOn && previous.includes(r.runsOn)));
|
|
299
440
|
}
|
|
300
441
|
/** Start the current workspace's scheduler; safe to call more than once. Ticks run in that workspace. */
|
|
301
442
|
export function startRoutineScheduler() {
|
|
@@ -5,7 +5,9 @@ export declare const agentRoutes: (import("./router.js").Route<"agents.addJourna
|
|
|
5
5
|
}, "/api/agents/:slug/journal", z.ZodObject<{
|
|
6
6
|
entry: z.ZodDefault<z.ZodString>;
|
|
7
7
|
kind: z.ZodOptional<z.ZodString>;
|
|
8
|
-
}, z.core.$strip>> | import("./router.js").Route<"agents.
|
|
8
|
+
}, z.core.$strip>> | import("./router.js").Route<"agents.adoptRoutine", import("../../core/routines.js").AdoptResult & {
|
|
9
|
+
routine: import("../../core/routines.js").RoutineStatus;
|
|
10
|
+
}, "/api/agents/:slug/routines/:id/adopt", undefined> | import("./router.js").Route<"agents.archive", import("../../core/agents.js").AgentDetail, "/api/agents/:slug/archive", z.ZodObject<{
|
|
9
11
|
archived: z.ZodDefault<z.ZodBoolean>;
|
|
10
12
|
}, z.core.$strip>> | import("./router.js").Route<"agents.create", import("../../core/agents.js").AgentDetail, "/api/agents", z.ZodObject<{
|
|
11
13
|
name: z.ZodOptional<z.ZodString>;
|
|
@@ -56,6 +58,8 @@ export declare const agentRoutes: (import("./router.js").Route<"agents.addJourna
|
|
|
56
58
|
schedule: z.ZodOptional<z.ZodString>;
|
|
57
59
|
prompt: z.ZodOptional<z.ZodString>;
|
|
58
60
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
61
|
+
runsOn: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
62
|
+
private: z.ZodOptional<z.ZodBoolean>;
|
|
59
63
|
}, z.core.$strip>> | import("./router.js").Route<"agents.saveSkill", import("../../core/skills.js").SkillInfo & {
|
|
60
64
|
content: string;
|
|
61
65
|
}, "/api/agents/:slug/skills", z.ZodObject<{
|
|
@@ -2,7 +2,7 @@ import * as z from "zod";
|
|
|
2
2
|
import { agentsRoot, deleteAgent, getAgent, listAgents, saveAgent, setAgentArchived } from "../../core/agents.js";
|
|
3
3
|
import { appendJournal, readJournal } from "../../core/journal.js";
|
|
4
4
|
import { UI_ACTOR } from "../../core/knowledge.js";
|
|
5
|
-
import { deleteRoutine, routineStatuses, runRoutineNow, saveRoutine } from "../../core/routines.js";
|
|
5
|
+
import { adoptRoutine, deleteRoutine, listRoutines, routineStatuses, runRoutineNow, saveRoutine } from "../../core/routines.js";
|
|
6
6
|
import { deleteAgentSkill, listAgentSkills, readSkill, saveAgentSkill } from "../../core/skills.js";
|
|
7
7
|
import { fail, route } from "./router.js";
|
|
8
8
|
/** A member's editable fields. Loose: saveAgent checks values (name, harness, effort) and keeps what it knows. */
|
|
@@ -21,13 +21,19 @@ const AgentFields = z.looseObject({
|
|
|
21
21
|
skills: z.array(z.string()).optional(),
|
|
22
22
|
system: z.string().optional(),
|
|
23
23
|
});
|
|
24
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* A routine: every field saveRoutine stores (it builds the routine from these alone), so a strict object loses nothing.
|
|
26
|
+
* saveRoutine checks name, prompt, the cron schedule and runsOn. runsOn absent keeps the stored runner (a new routine:
|
|
27
|
+
* this machine); null or "" unassigns it.
|
|
28
|
+
*/
|
|
25
29
|
const RoutineFields = z.object({
|
|
26
30
|
id: z.string().optional(),
|
|
27
31
|
name: z.string().optional(),
|
|
28
32
|
schedule: z.string().optional(),
|
|
29
33
|
prompt: z.string().optional(),
|
|
30
34
|
enabled: z.boolean().optional(),
|
|
35
|
+
runsOn: z.string().nullable().optional(),
|
|
36
|
+
private: z.boolean().optional(),
|
|
31
37
|
});
|
|
32
38
|
/** Agents (team members): definition, own skills, journal, routines. */
|
|
33
39
|
export const agentRoutes = [
|
|
@@ -92,5 +98,16 @@ export const agentRoutes = [
|
|
|
92
98
|
await deleteRoutine(c.params.slug, c.params.id);
|
|
93
99
|
return { ok: true };
|
|
94
100
|
}),
|
|
101
|
+
route({
|
|
102
|
+
name: "agents.adoptRoutine",
|
|
103
|
+
method: "POST",
|
|
104
|
+
path: "/api/agents/:slug/routines/:id/adopt",
|
|
105
|
+
summary: "run this routine here: runs_on = this machine's identity, routines.yml committed",
|
|
106
|
+
errors: 400,
|
|
107
|
+
}, async (c) => {
|
|
108
|
+
if (!(await listRoutines(c.params.slug)).some((r) => r.id === c.params.id))
|
|
109
|
+
fail(404, "routine not found");
|
|
110
|
+
return adoptRoutine(c.params.slug, c.params.id);
|
|
111
|
+
}),
|
|
95
112
|
route({ name: "agents.runRoutine", method: "POST", path: "/api/agents/:slug/routines/:id/run", summary: "run a routine now", errors: 400 }, async (c) => runRoutineNow(c.params.slug, c.params.id)),
|
|
96
113
|
];
|
|
@@ -27,7 +27,9 @@ export declare const routes: (Route<"agents.addJournal", {
|
|
|
27
27
|
}, "/api/agents/:slug/journal", z.ZodObject<{
|
|
28
28
|
entry: z.ZodDefault<z.ZodString>;
|
|
29
29
|
kind: z.ZodOptional<z.ZodString>;
|
|
30
|
-
}, z.core.$strip>> | Route<"agents.
|
|
30
|
+
}, z.core.$strip>> | Route<"agents.adoptRoutine", import("../../core/routines.js").AdoptResult & {
|
|
31
|
+
routine: import("../../core/routines.js").RoutineStatus;
|
|
32
|
+
}, "/api/agents/:slug/routines/:id/adopt", undefined> | Route<"agents.archive", import("../../core/agents.js").AgentDetail, "/api/agents/:slug/archive", z.ZodObject<{
|
|
31
33
|
archived: z.ZodDefault<z.ZodBoolean>;
|
|
32
34
|
}, z.core.$strip>> | Route<"agents.create", import("../../core/agents.js").AgentDetail, "/api/agents", z.ZodObject<{
|
|
33
35
|
name: z.ZodOptional<z.ZodString>;
|
|
@@ -78,6 +80,8 @@ export declare const routes: (Route<"agents.addJournal", {
|
|
|
78
80
|
schedule: z.ZodOptional<z.ZodString>;
|
|
79
81
|
prompt: z.ZodOptional<z.ZodString>;
|
|
80
82
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
83
|
+
runsOn: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
84
|
+
private: z.ZodOptional<z.ZodBoolean>;
|
|
81
85
|
}, z.core.$strip>> | Route<"agents.saveSkill", import("../../core/skills.js").SkillInfo & {
|
|
82
86
|
content: string;
|
|
83
87
|
}, "/api/agents/:slug/skills", z.ZodObject<{
|
|
@@ -399,7 +403,12 @@ export declare const routes: (Route<"agents.addJournal", {
|
|
|
399
403
|
aliasPort?: number | undefined;
|
|
400
404
|
}, "/api/hub/open", z.ZodObject<{
|
|
401
405
|
root: z.ZodString;
|
|
402
|
-
}, z.core.$strip>> | Route<"
|
|
406
|
+
}, z.core.$strip>> | Route<"identity.get", {
|
|
407
|
+
handle: string;
|
|
408
|
+
machine: string;
|
|
409
|
+
identity: string;
|
|
410
|
+
previous: string[];
|
|
411
|
+
}, "/api/identity", undefined> | Route<"knowledge.concept", import("../../okf.js").ConceptDetail, "/api/knowledge/:bundle/concept", undefined> | Route<"knowledge.create", import("../../okf.js").BundleInfo, "/api/knowledge", z.ZodObject<{
|
|
403
412
|
name: z.ZodDefault<z.ZodString>;
|
|
404
413
|
}, z.core.$strip>> | Route<"knowledge.delete", {
|
|
405
414
|
ok: boolean;
|
|
@@ -497,7 +506,9 @@ export declare const routes: (Route<"agents.addJournal", {
|
|
|
497
506
|
error?: string;
|
|
498
507
|
off?: boolean;
|
|
499
508
|
repo?: import("../../core/repos.js").RepoInfo;
|
|
500
|
-
}>, "/api/repos/:slug/update", undefined> | Route<"routines.
|
|
509
|
+
}>, "/api/repos/:slug/update", undefined> | Route<"routines.adopt", import("../../core/routines.js").AdoptResult, "/api/routines/adopt", z.ZodObject<{
|
|
510
|
+
retired: z.ZodDefault<z.ZodBoolean>;
|
|
511
|
+
}, z.core.$strip>> | Route<"routines.list", {
|
|
501
512
|
routines: (import("../../core/routines.js").RoutineStatus & {
|
|
502
513
|
agent: {
|
|
503
514
|
slug: string;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as z from "zod";
|
|
2
2
|
/** Workflows, their runs, and the routines that start them. */
|
|
3
|
-
export declare const runRoutes: (import("./router.js").Route<"routines.
|
|
3
|
+
export declare const runRoutes: (import("./router.js").Route<"routines.adopt", import("../../core/routines.js").AdoptResult, "/api/routines/adopt", z.ZodObject<{
|
|
4
|
+
retired: z.ZodDefault<z.ZodBoolean>;
|
|
5
|
+
}, z.core.$strip>> | import("./router.js").Route<"routines.list", {
|
|
4
6
|
routines: (import("../../core/routines.js").RoutineStatus & {
|
|
5
7
|
agent: {
|
|
6
8
|
slug: string;
|
package/dist/server/api/runs.js
CHANGED
|
@@ -2,7 +2,7 @@ import { promises as fs } from "node:fs";
|
|
|
2
2
|
import * as z from "zod";
|
|
3
3
|
import { getOutputDir } from "../../core/context.js";
|
|
4
4
|
import { decide } from "../../core/decisions.js";
|
|
5
|
-
import { allRoutines } from "../../core/routines.js";
|
|
5
|
+
import { adoptUnassigned, allRoutines } from "../../core/routines.js";
|
|
6
6
|
import { dockerStatus, stopRun, triggerRun } from "../../core/runner.js";
|
|
7
7
|
import { deleteRun, getRun, listRuns, readRunFile, safeRunDir } from "../../core/runs.js";
|
|
8
8
|
import { deleteWorkflow, getWorkflow, listWorkflows } from "../../core/workflows.js";
|
|
@@ -59,6 +59,14 @@ export const runRoutes = [
|
|
|
59
59
|
route({ name: "routines.list", method: "GET", path: "/api/routines", summary: "every agent's routines with run state and the workflows each one names" }, async () => ({
|
|
60
60
|
routines: await allRoutines((await listWorkflows()).workflows),
|
|
61
61
|
})),
|
|
62
|
+
route({
|
|
63
|
+
name: "routines.adopt",
|
|
64
|
+
method: "POST",
|
|
65
|
+
path: "/api/routines/adopt",
|
|
66
|
+
summary: "run these here: every enabled routine without runs_on (and, with {retired: true}, those naming a retired identity) gets this machine; routines.yml committed",
|
|
67
|
+
errors: 400,
|
|
68
|
+
body: z.object({ retired: z.boolean().default(false) }),
|
|
69
|
+
}, async (c) => adoptUnassigned({ retired: (await c.body()).retired })),
|
|
62
70
|
route({ name: "runs.list", method: "GET", path: "/api/runs", summary: "every run in the output dir" }, async () => ({ outputDir: getOutputDir(), runs: await listRuns() })),
|
|
63
71
|
route({ name: "runs.get", method: "GET", path: "/api/runs/:id", summary: "one run: phases, gates, files" }, async (c) => (await validRun(getRun(c.params.id))) ?? fail(404, "not found")),
|
|
64
72
|
route({ name: "runs.fileText", method: "GET", path: "/api/runs/:id/text", summary: "one run file's text ?name=, tail-capped" }, async (c) => {
|
|
@@ -6,7 +6,12 @@ type StartReply = StartResult & {
|
|
|
6
6
|
here?: true;
|
|
7
7
|
};
|
|
8
8
|
/** The workspace itself: identity and features, settings, the other workspaces, updates. */
|
|
9
|
-
export declare const workspaceRoutes: (import("./router.js").Route<"
|
|
9
|
+
export declare const workspaceRoutes: (import("./router.js").Route<"identity.get", {
|
|
10
|
+
handle: string;
|
|
11
|
+
machine: string;
|
|
12
|
+
identity: string;
|
|
13
|
+
previous: string[];
|
|
14
|
+
}, "/api/identity", undefined> | import("./router.js").Route<"meta.get", {
|
|
10
15
|
workspaceName: string;
|
|
11
16
|
workspaceSlug: string;
|
|
12
17
|
workspaceIcon: string;
|
|
@@ -11,6 +11,7 @@ import { getSettings, saveSettings } from "../../core/settings.js";
|
|
|
11
11
|
import { canSelfUpdate, startUpdate, updateStatus } from "../../core/update.js";
|
|
12
12
|
import { getDiskVersion, getPkgName, getPkgVersion, RESTART_EXIT_CODE, supervised } from "../version.js";
|
|
13
13
|
import { disposeAllDevs } from "../../core/vibeables.js";
|
|
14
|
+
import { identityString, readIdentity } from "../../core/identity.js";
|
|
14
15
|
import { fail, reply, route } from "./router.js";
|
|
15
16
|
/** A feature block with a folder (repos, vibeables, projects); saveSettings checks the root. Nested blocks are plain objects: saveSettings reads only these fields. */
|
|
16
17
|
const RootBlock = z.object({ enabled: z.boolean(), root: z.string().optional() });
|
|
@@ -108,6 +109,10 @@ export const workspaceRoutes = [
|
|
|
108
109
|
switcher,
|
|
109
110
|
};
|
|
110
111
|
}),
|
|
112
|
+
route({ name: "identity.get", method: "GET", path: "/api/identity", summary: "this machine's identity (handle@machine) — what routines name in runs_on" }, async () => {
|
|
113
|
+
const id = await readIdentity();
|
|
114
|
+
return { handle: id.handle, machine: id.machine, identity: identityString(id), previous: id.previous };
|
|
115
|
+
}),
|
|
111
116
|
route({ name: "settings.get", method: "GET", path: "/api/settings", summary: "kraftwerk.yml, parsed, with resolved paths" }, async () => getSettings()),
|
|
112
117
|
route({ name: "settings.save", method: "PUT", path: "/api/settings", summary: "write the UI-editable subset of kraftwerk.yml", errors: 400, body: SettingsFields }, async (c) => saveSettings((await c.body()))),
|
|
113
118
|
route({ name: "workspaces.list", method: "GET", path: "/api/workspaces", summary: "every known workspace incl. this one, with state and counts" }, async () => placeInHub(await listWorkspacesDetailed())),
|
package/dist/server/push.js
CHANGED
|
@@ -11,7 +11,7 @@ export function sealPush(keyBase64, content) {
|
|
|
11
11
|
const ct = Buffer.concat([cipher.update(JSON.stringify(content), "utf8"), cipher.final()]);
|
|
12
12
|
return Buffer.concat([nonce, ct, cipher.getAuthTag()]).toString("base64");
|
|
13
13
|
}
|
|
14
|
-
const LABEL = { approval: "Approval", question: "Question", failed: "Failed" };
|
|
14
|
+
const LABEL = { approval: "Approval", question: "Question", failed: "Failed", runner: "Routines" };
|
|
15
15
|
export function contentOf(item, workspace) {
|
|
16
16
|
return {
|
|
17
17
|
title: `${LABEL[item.kind] ?? "Needs you"} · ${workspace.name}`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netnodeag/kraftwerk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.76.0",
|
|
4
4
|
"description": "Open-source agentic workspace for teams — where people and AI agents get work done together: agents (Claude Code, Codex, Pi), skills, shared knowledge, repeatable workflows, and deterministic verification",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
"type": "object",
|
|
7
7
|
"additionalProperties": false,
|
|
8
8
|
"properties": {
|
|
9
|
+
"id": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"pattern": "^ws_[a-z0-9]{10}$",
|
|
12
|
+
"description": "The workspace's identity on every machine and clone: ws_ followed by 10 lowercase base36 characters. Committed with kraftwerk.yml; `kraftwerk init` writes it, `kraftwerk doctor --fix` adds a missing one."
|
|
13
|
+
},
|
|
9
14
|
"name": {
|
|
10
15
|
"type": "string",
|
|
11
16
|
"description": "Display name of the project (\"environment\"), shown in the inspector header and browser tab"
|