@netnodeag/kraftwerk 0.4.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Routines: per-team-member scheduled prompts — "message your agent every
3
+ * weekday at 9". Definitions live next to the member (git-tracked):
4
+ *
5
+ * agents/<slug>/routines.yml # list of { id, name, schedule, prompt, enabled }
6
+ *
7
+ * Runtime state (last run / last session / errors) is run-state, not
8
+ * config, so it lives in <output>/routines-state.json. The scheduler is a
9
+ * plain interval inside the inspector server: each due routine opens a new
10
+ * session (chat) for the member and posts the prompt — the run shows up in
11
+ * the member's sessions sidebar like any conversation. Schedules use
12
+ * standard 5-field cron in the server's local time.
13
+ */
14
+ export interface Routine {
15
+ id: string;
16
+ name: string;
17
+ /** 5-field cron (min hour dom month dow) or @hourly/@daily/@weekly/@monthly. */
18
+ schedule: string;
19
+ prompt: string;
20
+ enabled: boolean;
21
+ }
22
+ export interface RoutineStatus extends Routine {
23
+ nextRunAt?: string;
24
+ lastRunAt?: string;
25
+ lastChatId?: string;
26
+ lastError?: string;
27
+ }
28
+ interface CronSpec {
29
+ minute: Set<number>;
30
+ hour: Set<number>;
31
+ dom: Set<number>;
32
+ month: Set<number>;
33
+ dow: Set<number>;
34
+ domStar: boolean;
35
+ dowStar: boolean;
36
+ }
37
+ export declare function parseCron(expr: string): CronSpec;
38
+ /** Next matching instant after `from`, scanning up to 60 days; undefined if none. */
39
+ export declare function nextRun(schedule: string, from?: Date): string | undefined;
40
+ export declare function listRoutines(slug: string): Promise<Routine[]>;
41
+ export interface SaveRoutineInput {
42
+ id?: string;
43
+ name: string;
44
+ schedule: string;
45
+ prompt: string;
46
+ enabled?: boolean;
47
+ }
48
+ export declare function saveRoutine(slug: string, input: SaveRoutineInput): Promise<Routine>;
49
+ export declare function deleteRoutine(slug: string, id: string): Promise<void>;
50
+ export declare function routineStatuses(slug: string): Promise<RoutineStatus[]>;
51
+ export declare function runRoutineNow(slug: string, id: string): Promise<{
52
+ chatId: string;
53
+ }>;
54
+ /** Start the in-process scheduler; safe to call more than once. */
55
+ export declare function startRoutineScheduler(): void;
56
+ export {};
@@ -0,0 +1,266 @@
1
+ import { promises as fs } from "node:fs";
2
+ import path from "node:path";
3
+ import { parse, stringify } from "yaml";
4
+ import { getOutputDir } from "./context.js";
5
+ import { getMember, listMembers, safeMemberSlug, teamRoot } from "./team.js";
6
+ import { createChat, postMessage } from "./chat/sessions.js";
7
+ /* ---------- cron ---------- */
8
+ const ALIASES = {
9
+ "@hourly": "0 * * * *",
10
+ "@daily": "0 0 * * *",
11
+ "@weekly": "0 0 * * 0",
12
+ "@monthly": "0 0 1 * *",
13
+ };
14
+ function parseField(spec, min, max, label) {
15
+ const out = new Set();
16
+ for (const part of spec.split(",")) {
17
+ const m = /^(\*|\d+(?:-\d+)?)(?:\/(\d+))?$/.exec(part);
18
+ if (!m)
19
+ throw new Error(`invalid ${label} field "${part}"`);
20
+ const step = m[2] ? Number(m[2]) : 1;
21
+ if (step < 1)
22
+ throw new Error(`invalid step in ${label} field`);
23
+ let lo = min;
24
+ let hi = max;
25
+ if (m[1] !== "*") {
26
+ const [a, b] = m[1].split("-").map(Number);
27
+ lo = a;
28
+ hi = b ?? (m[2] ? max : a); // "a/n" means a..max stepped; bare "a" is exact
29
+ }
30
+ if (lo < min || hi > max || lo > hi)
31
+ throw new Error(`${label} field out of range (${min}-${max})`);
32
+ for (let v = lo; v <= hi; v += step)
33
+ out.add(v);
34
+ }
35
+ return out;
36
+ }
37
+ export function parseCron(expr) {
38
+ const fields = (ALIASES[expr.trim()] ?? expr.trim()).split(/\s+/);
39
+ if (fields.length !== 5) {
40
+ throw new Error("schedule must be 5 cron fields (min hour dom month dow) or an @alias");
41
+ }
42
+ const spec = {
43
+ minute: parseField(fields[0], 0, 59, "minute"),
44
+ hour: parseField(fields[1], 0, 23, "hour"),
45
+ dom: parseField(fields[2], 1, 31, "day-of-month"),
46
+ month: parseField(fields[3], 1, 12, "month"),
47
+ dow: new Set(
48
+ // 7 is an alias for Sunday (0)
49
+ [...parseField(fields[4], 0, 7, "day-of-week")].map((v) => (v === 7 ? 0 : v))),
50
+ domStar: fields[2] === "*",
51
+ dowStar: fields[4] === "*",
52
+ };
53
+ return spec;
54
+ }
55
+ function cronMatches(spec, d) {
56
+ if (!spec.minute.has(d.getMinutes()))
57
+ return false;
58
+ if (!spec.hour.has(d.getHours()))
59
+ return false;
60
+ if (!spec.month.has(d.getMonth() + 1))
61
+ return false;
62
+ const domOk = spec.dom.has(d.getDate());
63
+ const dowOk = spec.dow.has(d.getDay());
64
+ // Standard cron rule: if both day fields are restricted, either may match.
65
+ if (!spec.domStar && !spec.dowStar)
66
+ return domOk || dowOk;
67
+ return domOk && dowOk;
68
+ }
69
+ /** Next matching instant after `from`, scanning up to 60 days; undefined if none. */
70
+ export function nextRun(schedule, from = new Date()) {
71
+ let spec;
72
+ try {
73
+ spec = parseCron(schedule);
74
+ }
75
+ catch {
76
+ return undefined;
77
+ }
78
+ const d = new Date(from);
79
+ d.setSeconds(0, 0);
80
+ for (let i = 0; i < 60 * 24 * 60; i++) {
81
+ d.setMinutes(d.getMinutes() + 1);
82
+ if (cronMatches(spec, d))
83
+ return d.toISOString();
84
+ }
85
+ return undefined;
86
+ }
87
+ /* ---------- definition CRUD (agents/<slug>/routines.yml) ---------- */
88
+ async function routinesFile(slug) {
89
+ return path.join(await teamRoot(), safeMemberSlug(slug), "routines.yml");
90
+ }
91
+ function normalizeRoutine(raw) {
92
+ const id = String(raw.id ?? "").trim();
93
+ if (!id)
94
+ return null;
95
+ return {
96
+ id,
97
+ name: String(raw.name ?? id),
98
+ schedule: String(raw.schedule ?? ""),
99
+ prompt: String(raw.prompt ?? ""),
100
+ enabled: raw.enabled !== false,
101
+ };
102
+ }
103
+ export async function listRoutines(slug) {
104
+ let raw;
105
+ try {
106
+ raw = parse(await fs.readFile(await routinesFile(slug), "utf8"));
107
+ }
108
+ catch {
109
+ return [];
110
+ }
111
+ if (!Array.isArray(raw))
112
+ return [];
113
+ return raw
114
+ .map((r) => normalizeRoutine((r ?? {})))
115
+ .filter(Boolean);
116
+ }
117
+ async function writeRoutines(slug, routines) {
118
+ await fs.writeFile(await routinesFile(slug), stringify(routines));
119
+ }
120
+ export async function saveRoutine(slug, input) {
121
+ const name = input.name?.trim();
122
+ if (!name)
123
+ throw new Error("name is required");
124
+ if (!input.prompt?.trim())
125
+ throw new Error("prompt is required");
126
+ parseCron(input.schedule); // throws with a useful message on a bad schedule
127
+ const id = input.id?.trim() ||
128
+ name
129
+ .toLowerCase()
130
+ .replace(/[^a-z0-9]+/g, "-")
131
+ .replace(/^-+|-+$/g, "")
132
+ .slice(0, 48);
133
+ if (!id)
134
+ throw new Error("name must contain at least one letter or digit");
135
+ const routine = {
136
+ id,
137
+ name,
138
+ schedule: input.schedule.trim(),
139
+ prompt: input.prompt,
140
+ enabled: input.enabled !== false,
141
+ };
142
+ const routines = await listRoutines(slug);
143
+ const i = routines.findIndex((r) => r.id === id);
144
+ if (i >= 0)
145
+ routines[i] = routine;
146
+ else
147
+ routines.push(routine);
148
+ await writeRoutines(slug, routines);
149
+ return routine;
150
+ }
151
+ export async function deleteRoutine(slug, id) {
152
+ const routines = await listRoutines(slug);
153
+ await writeRoutines(slug, routines.filter((r) => r.id !== id));
154
+ }
155
+ const stateFile = () => path.join(getOutputDir(), "routines-state.json");
156
+ async function readState() {
157
+ try {
158
+ return JSON.parse(await fs.readFile(stateFile(), "utf8"));
159
+ }
160
+ catch {
161
+ return {};
162
+ }
163
+ }
164
+ async function writeState(state) {
165
+ await fs.mkdir(getOutputDir(), { recursive: true });
166
+ await fs.writeFile(stateFile(), JSON.stringify(state, null, 2));
167
+ }
168
+ export async function routineStatuses(slug) {
169
+ const [routines, state] = await Promise.all([listRoutines(slug), readState()]);
170
+ return routines.map((r) => {
171
+ const s = state[`${slug}/${r.id}`] ?? {};
172
+ return {
173
+ ...r,
174
+ ...(r.enabled ? { nextRunAt: nextRun(r.schedule) } : {}),
175
+ ...(s.lastRunAt ? { lastRunAt: s.lastRunAt } : {}),
176
+ ...(s.lastChatId ? { lastChatId: s.lastChatId } : {}),
177
+ ...(s.lastError ? { lastError: s.lastError } : {}),
178
+ };
179
+ });
180
+ }
181
+ /* ---------- firing ---------- */
182
+ /** Open a fresh session for the member and post the routine's prompt. */
183
+ async function fireRoutine(slug, routine) {
184
+ const member = await getMember(slug);
185
+ if (!member)
186
+ throw new Error(`team agent "${slug}" not found`);
187
+ const meta = await createChat({
188
+ agent: member.harness,
189
+ scope: { kind: "team", member: slug, routine: routine.id },
190
+ title: `⏰ ${routine.name}`,
191
+ });
192
+ const { error } = await postMessage(meta.id, routine.prompt);
193
+ if (error)
194
+ throw new Error(error);
195
+ return meta.id;
196
+ }
197
+ export async function runRoutineNow(slug, id) {
198
+ const routine = (await listRoutines(slug)).find((r) => r.id === id);
199
+ if (!routine)
200
+ throw new Error("routine not found");
201
+ const state = await readState();
202
+ const key = `${slug}/${id}`;
203
+ try {
204
+ const chatId = await fireRoutine(slug, routine);
205
+ state[key] = { ...state[key], lastRunAt: new Date().toISOString(), lastChatId: chatId };
206
+ delete state[key].lastError;
207
+ await writeState(state);
208
+ return { chatId };
209
+ }
210
+ catch (err) {
211
+ state[key] = { ...state[key], lastError: err.message };
212
+ await writeState(state);
213
+ throw err;
214
+ }
215
+ }
216
+ /* ---------- scheduler ---------- */
217
+ let timer = null;
218
+ async function tick() {
219
+ const now = new Date();
220
+ const p = (n) => String(n).padStart(2, "0");
221
+ const minuteKey = `${now.getFullYear()}-${p(now.getMonth() + 1)}-${p(now.getDate())}T${p(now.getHours())}:${p(now.getMinutes())}`;
222
+ let members;
223
+ try {
224
+ members = await listMembers();
225
+ }
226
+ catch {
227
+ return;
228
+ }
229
+ const state = await readState();
230
+ let dirty = false;
231
+ for (const m of members) {
232
+ for (const r of await listRoutines(m.slug).catch(() => [])) {
233
+ if (!r.enabled)
234
+ continue;
235
+ let due = false;
236
+ try {
237
+ due = cronMatches(parseCron(r.schedule), now);
238
+ }
239
+ catch {
240
+ continue; // invalid schedule — surfaced by validation on save, skip here
241
+ }
242
+ const key = `${m.slug}/${r.id}`;
243
+ if (!due || state[key]?.lastMinute === minuteKey)
244
+ continue;
245
+ state[key] = { ...state[key], lastMinute: minuteKey };
246
+ dirty = true;
247
+ try {
248
+ const chatId = await fireRoutine(m.slug, r);
249
+ state[key] = { ...state[key], lastRunAt: now.toISOString(), lastChatId: chatId };
250
+ delete state[key].lastError;
251
+ }
252
+ catch (err) {
253
+ state[key] = { ...state[key], lastError: err.message };
254
+ }
255
+ }
256
+ }
257
+ if (dirty)
258
+ await writeState(state);
259
+ }
260
+ /** Start the in-process scheduler; safe to call more than once. */
261
+ export function startRoutineScheduler() {
262
+ if (timer)
263
+ return;
264
+ timer = setInterval(() => void tick().catch(() => { }), 20_000);
265
+ timer.unref?.();
266
+ }
@@ -5,6 +5,10 @@ import { setOutputDir, getOutputDir } from "./context.js";
5
5
  import { listRuns, getRun, readRunFile } from "./runs.js";
6
6
  import { listWorkflows, getWorkflow } from "./workflows.js";
7
7
  import { dockerStatus, triggerRun, stopRun } from "./runner.js";
8
+ import { cancelChat, createChat, getChat, listChats, postMessage, resolvePermission, subscribeChat, } from "./chat/sessions.js";
9
+ import { bundleDetail, conceptDetail, createBundle, knowledgeIndex, putConcept, verifyFromUi, } from "./knowledge.js";
10
+ import { deleteMember, getMember, listMembers, saveMember, teamRoot, } from "./team.js";
11
+ import { deleteRoutine, routineStatuses, runRoutineNow, saveRoutine, startRoutineScheduler, } from "./routines.js";
8
12
  /**
9
13
  * The inspector server: a plain node:http server with no dependencies.
10
14
  * Serves the prebuilt SPA (inspector/dist) plus the JSON/file API the
@@ -31,6 +35,9 @@ const MIME = {
31
35
  /** Text preview payloads are capped; the tail matters most for logs. */
32
36
  const MAX_TEXT = 400_000;
33
37
  function json(res, body, status = 200) {
38
+ // A late error on an SSE response must not try to write headers again.
39
+ if (res.headersSent)
40
+ return void res.end();
34
41
  res.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
35
42
  res.end(JSON.stringify(body));
36
43
  }
@@ -140,6 +147,270 @@ async function handleApi(req, res, url) {
140
147
  }
141
148
  }
142
149
  }
150
+ // GET/POST /api/knowledge — bundle index / create a bundle
151
+ if (seg.length === 2 && seg[1] === "knowledge") {
152
+ if (method === "GET")
153
+ return json(res, await knowledgeIndex());
154
+ if (method === "POST") {
155
+ let body;
156
+ try {
157
+ body = JSON.parse(await readBody(req));
158
+ }
159
+ catch {
160
+ return json(res, { error: "invalid JSON body" }, 400);
161
+ }
162
+ try {
163
+ return json(res, await createBundle(String(body.name ?? "").trim()));
164
+ }
165
+ catch (err) {
166
+ return json(res, { error: err.message }, 400);
167
+ }
168
+ }
169
+ }
170
+ // GET /api/knowledge/:bundle — concepts + log
171
+ if (seg.length === 3 && seg[1] === "knowledge" && method === "GET") {
172
+ try {
173
+ const detail = await bundleDetail(decodeURIComponent(seg[2]));
174
+ return detail ? json(res, detail) : json(res, { error: "not found" }, 404);
175
+ }
176
+ catch {
177
+ return json(res, { error: "invalid bundle name" }, 400);
178
+ }
179
+ }
180
+ // GET/POST /api/knowledge/:bundle/concept?id=... — read / write one concept
181
+ if (seg.length === 4 && seg[1] === "knowledge" && seg[3] === "concept") {
182
+ const bundle = decodeURIComponent(seg[2]);
183
+ if (method === "GET") {
184
+ const id = url.searchParams.get("id") ?? "";
185
+ try {
186
+ const concept = await conceptDetail(bundle, id);
187
+ return concept ? json(res, concept) : json(res, { error: "not found" }, 404);
188
+ }
189
+ catch (err) {
190
+ return json(res, { error: err.message }, 400);
191
+ }
192
+ }
193
+ if (method === "POST") {
194
+ let body;
195
+ try {
196
+ body = JSON.parse(await readBody(req));
197
+ }
198
+ catch {
199
+ return json(res, { error: "invalid JSON body" }, 400);
200
+ }
201
+ if (!body.id || !body.content?.trim()) {
202
+ return json(res, { error: "id and content are required" }, 400);
203
+ }
204
+ try {
205
+ return json(res, await putConcept(bundle, body.id, body.content));
206
+ }
207
+ catch (err) {
208
+ return json(res, { error: err.message }, 400);
209
+ }
210
+ }
211
+ }
212
+ // POST /api/knowledge/:bundle/verify — human verification from the UI
213
+ if (seg.length === 4 && seg[1] === "knowledge" && seg[3] === "verify" && method === "POST") {
214
+ let body;
215
+ try {
216
+ body = JSON.parse(await readBody(req));
217
+ }
218
+ catch {
219
+ return json(res, { error: "invalid JSON body" }, 400);
220
+ }
221
+ if (!body.id)
222
+ return json(res, { error: "id is required" }, 400);
223
+ try {
224
+ return json(res, await verifyFromUi(decodeURIComponent(seg[2]), body.id));
225
+ }
226
+ catch (err) {
227
+ return json(res, { error: err.message }, 400);
228
+ }
229
+ }
230
+ // GET/POST /api/team — member list / create a member
231
+ if (seg.length === 2 && seg[1] === "team") {
232
+ if (method === "GET") {
233
+ return json(res, { root: await teamRoot(), members: await listMembers() });
234
+ }
235
+ if (method === "POST") {
236
+ try {
237
+ const body = JSON.parse(await readBody(req));
238
+ return json(res, await saveMember({ ...body, slug: undefined }));
239
+ }
240
+ catch (err) {
241
+ return json(res, { error: err.message }, 400);
242
+ }
243
+ }
244
+ }
245
+ // GET/POST /api/team/:slug/routines — list (with run state) / upsert
246
+ if (seg.length === 4 && seg[1] === "team" && seg[3] === "routines") {
247
+ const slug = decodeURIComponent(seg[2]);
248
+ try {
249
+ if (method === "GET")
250
+ return json(res, { routines: await routineStatuses(slug) });
251
+ if (method === "POST") {
252
+ const body = JSON.parse(await readBody(req));
253
+ return json(res, await saveRoutine(slug, body));
254
+ }
255
+ }
256
+ catch (err) {
257
+ return json(res, { error: err.message }, 400);
258
+ }
259
+ }
260
+ // DELETE /api/team/:slug/routines/:id | POST /api/team/:slug/routines/:id/run
261
+ if (seg.length >= 5 && seg[1] === "team" && seg[3] === "routines") {
262
+ const slug = decodeURIComponent(seg[2]);
263
+ const id = decodeURIComponent(seg[4]);
264
+ try {
265
+ if (seg.length === 5 && method === "DELETE") {
266
+ await deleteRoutine(slug, id);
267
+ return json(res, { ok: true });
268
+ }
269
+ if (seg.length === 6 && seg[5] === "run" && method === "POST") {
270
+ return json(res, await runRoutineNow(slug, id));
271
+ }
272
+ }
273
+ catch (err) {
274
+ return json(res, { error: err.message }, 400);
275
+ }
276
+ }
277
+ // GET/PUT/DELETE /api/team/:slug
278
+ if (seg.length === 3 && seg[1] === "team") {
279
+ const slug = decodeURIComponent(seg[2]);
280
+ try {
281
+ if (method === "GET") {
282
+ const member = await getMember(slug);
283
+ return member ? json(res, member) : json(res, { error: "not found" }, 404);
284
+ }
285
+ if (method === "PUT") {
286
+ const body = JSON.parse(await readBody(req));
287
+ return json(res, await saveMember({ ...body, slug }));
288
+ }
289
+ if (method === "DELETE") {
290
+ await deleteMember(slug);
291
+ return json(res, { ok: true });
292
+ }
293
+ }
294
+ catch (err) {
295
+ return json(res, { error: err.message }, 400);
296
+ }
297
+ }
298
+ // GET/POST /api/chats
299
+ if (seg.length === 2 && seg[1] === "chats") {
300
+ if (method === "GET")
301
+ return json(res, { chats: await listChats() });
302
+ if (method === "POST") {
303
+ let body;
304
+ try {
305
+ body = JSON.parse(await readBody(req));
306
+ }
307
+ catch {
308
+ return json(res, { error: "invalid JSON body" }, 400);
309
+ }
310
+ let agent = body.agent;
311
+ let scope;
312
+ if (body.scope?.kind === "team" && body.scope.member) {
313
+ // Team sessions run on the member's configured harness.
314
+ const member = await getMember(body.scope.member).catch(() => null);
315
+ if (!member)
316
+ return json(res, { error: "team agent not found" }, 404);
317
+ scope = { kind: "team", member: member.slug };
318
+ agent = member.harness;
319
+ }
320
+ else if (body.scope?.kind === "run" && body.scope.runId) {
321
+ scope = { kind: "run", runId: body.scope.runId };
322
+ }
323
+ else if (body.scope?.kind === "kraftwerk") {
324
+ scope = { kind: "kraftwerk" };
325
+ }
326
+ else if (body.scope?.kind === "knowledge") {
327
+ scope = { kind: "knowledge", ...(body.scope.bundle ? { bundle: body.scope.bundle } : {}) };
328
+ }
329
+ else {
330
+ scope = { kind: "general" };
331
+ }
332
+ if (!["claude", "codex", "pi"].includes(agent)) {
333
+ return json(res, { error: "agent must be claude, codex, or pi" }, 400);
334
+ }
335
+ try {
336
+ return json(res, await createChat({ agent, scope }));
337
+ }
338
+ catch (err) {
339
+ return json(res, { error: err.message }, 400);
340
+ }
341
+ }
342
+ }
343
+ // GET /api/chats/:id
344
+ if (seg.length === 3 && seg[1] === "chats" && method === "GET") {
345
+ try {
346
+ const chat = await getChat(seg[2]);
347
+ return chat ? json(res, chat) : json(res, { error: "not found" }, 404);
348
+ }
349
+ catch {
350
+ return json(res, { error: "invalid chat id" }, 400);
351
+ }
352
+ }
353
+ // GET /api/chats/:id/events?after=N — SSE stream of thread events.
354
+ if (seg.length === 4 && seg[1] === "chats" && seg[3] === "events" && method === "GET") {
355
+ const afterSeq = Number(url.searchParams.get("after") ?? "0") || 0;
356
+ const send = (ev) => {
357
+ res.write(`id: ${ev.seq}\ndata: ${JSON.stringify(ev)}\n\n`);
358
+ };
359
+ let unsubscribe = null;
360
+ try {
361
+ res.writeHead(200, {
362
+ "content-type": "text/event-stream",
363
+ "cache-control": "no-store",
364
+ connection: "keep-alive",
365
+ });
366
+ unsubscribe = await subscribeChat(seg[2], afterSeq, send);
367
+ }
368
+ catch {
369
+ return void res.end();
370
+ }
371
+ if (!unsubscribe)
372
+ return void res.end();
373
+ const heartbeat = setInterval(() => res.write(":hb\n\n"), 25_000);
374
+ req.on("close", () => {
375
+ clearInterval(heartbeat);
376
+ unsubscribe?.();
377
+ });
378
+ return;
379
+ }
380
+ // POST /api/chats/:id/{message,permission,cancel}
381
+ if (seg.length === 4 && seg[1] === "chats" && method === "POST") {
382
+ let body = {};
383
+ try {
384
+ const raw = await readBody(req);
385
+ if (raw)
386
+ body = JSON.parse(raw);
387
+ }
388
+ catch {
389
+ return json(res, { error: "invalid JSON body" }, 400);
390
+ }
391
+ try {
392
+ let result;
393
+ if (seg[3] === "message") {
394
+ const text = String(body.text ?? "").trim();
395
+ if (!text)
396
+ return json(res, { error: "text is required" }, 400);
397
+ result = await postMessage(seg[2], text);
398
+ }
399
+ else if (seg[3] === "permission") {
400
+ result = await resolvePermission(seg[2], String(body.requestId ?? ""), body.optionId == null ? null : String(body.optionId));
401
+ }
402
+ else if (seg[3] === "cancel") {
403
+ result = await cancelChat(seg[2]);
404
+ }
405
+ else {
406
+ return json(res, { error: "not found" }, 404);
407
+ }
408
+ return result.error ? json(res, result, 409) : json(res, { ok: true });
409
+ }
410
+ catch (err) {
411
+ return json(res, { error: err.message }, 400);
412
+ }
413
+ }
143
414
  json(res, { error: "not found" }, 404);
144
415
  }
145
416
  async function serveStatic(res, staticDir, pathname) {
@@ -167,6 +438,7 @@ async function serveStatic(res, staticDir, pathname) {
167
438
  /** Start the server; resolves once it listens. Runs until the process ends. */
168
439
  export function startInspector(opts) {
169
440
  setOutputDir(opts.outputDir);
441
+ startRoutineScheduler();
170
442
  const server = http.createServer(async (req, res) => {
171
443
  try {
172
444
  const url = new URL(req.url ?? "/", "http://localhost");
@@ -0,0 +1,50 @@
1
+ import type { ChatAgentId } from "./chat/types.js";
2
+ /**
3
+ * Team: persistent agent teammates, defined on the filesystem. Each member
4
+ * is one folder under the project's agents/ root:
5
+ *
6
+ * agents/<slug>/agent.yml # name, emoji, harness, model, effort, workflows
7
+ * agents/<slug>/system.md # the member's system prompt / role description
8
+ *
9
+ * Definitions are git-tracked project config (like workflows/), not run
10
+ * state — sessions with a member are ordinary chats scoped to it.
11
+ */
12
+ export declare const HARNESSES: ChatAgentId[];
13
+ export declare const EFFORTS: readonly ["low", "medium", "high", "xhigh", "max"];
14
+ export interface TeamMember {
15
+ slug: string;
16
+ name: string;
17
+ emoji: string;
18
+ description?: string;
19
+ harness: ChatAgentId;
20
+ model?: string;
21
+ effort?: string;
22
+ /** Workflow slugs this member knows about and may run. */
23
+ workflows: string[];
24
+ /** Knowledge bundles (OKF) this member works with. */
25
+ knowledge: string[];
26
+ }
27
+ export interface TeamMemberDetail extends TeamMember {
28
+ /** Contents of system.md — the member's system prompt. */
29
+ system: string;
30
+ }
31
+ export declare function teamRoot(): Promise<string>;
32
+ export declare function safeMemberSlug(slug: string): string;
33
+ /** Derive a slug from a display name: "Max Müller" -> "max-mueller"-ish. */
34
+ export declare function slugFromName(name: string): string;
35
+ export declare function listMembers(): Promise<TeamMember[]>;
36
+ export declare function getMember(slug: string): Promise<TeamMemberDetail | null>;
37
+ export interface SaveMemberInput {
38
+ slug?: string;
39
+ name: string;
40
+ emoji?: string;
41
+ description?: string;
42
+ harness: string;
43
+ model?: string;
44
+ effort?: string;
45
+ workflows?: string[];
46
+ knowledge?: string[];
47
+ system?: string;
48
+ }
49
+ export declare function saveMember(input: SaveMemberInput): Promise<TeamMemberDetail>;
50
+ export declare function deleteMember(slug: string): Promise<void>;