u-foo 1.2.14 → 1.3.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/modules/online/README.md +18 -0
- package/package.json +2 -1
- package/src/agent/cliRunner.js +1 -1
- package/src/agent/launcher.js +23 -4
- package/src/agent/ptyRunner.js +39 -16
- package/src/agent/ufooAgent.js +2 -1
- package/src/assistant/agent.js +2 -1
- package/src/assistant/bridge.js +9 -3
- package/src/assistant/constants.js +15 -0
- package/src/assistant/engine.js +7 -2
- package/src/assistant/ufooEngineCli.js +9 -3
- package/src/chat/commandExecutor.js +188 -13
- package/src/chat/commands.js +11 -0
- package/src/chat/daemonMessageRouter.js +107 -0
- package/src/cli/groupCoreCommands.js +246 -0
- package/src/cli/onlineCoreCommands.js +8 -0
- package/src/cli.js +325 -2
- package/src/code/agent.js +74 -3
- package/src/code/tui.js +73 -5
- package/src/daemon/groupOrchestrator.js +557 -0
- package/src/daemon/index.js +319 -1
- package/src/daemon/status.js +48 -0
- package/src/group/diagram.js +222 -0
- package/src/group/templates.js +280 -0
- package/src/group/validateTemplate.js +234 -0
- package/src/online/bridge.js +8 -1
- package/src/online/server.js +193 -14
- package/src/shared/eventContract.js +5 -0
- package/src/ufoo/paths.js +2 -0
- package/templates/groups/dev-basic.json +78 -0
- package/templates/groups/research-quick.json +49 -0
package/src/online/server.js
CHANGED
|
@@ -45,6 +45,9 @@ class OnlineServer extends EventEmitter {
|
|
|
45
45
|
|
|
46
46
|
this.rooms = new Map();
|
|
47
47
|
this.roomPasswords = new Map();
|
|
48
|
+
this.channelMessageHistory = new Map();
|
|
49
|
+
this.channelHistoryLimit = options.channelHistoryLimit ?? 200;
|
|
50
|
+
this.eventSeq = 0;
|
|
48
51
|
|
|
49
52
|
// Step 2 + 3: Payload limits
|
|
50
53
|
this.maxHttpBodyBytes = options.maxHttpBodyBytes ?? 65536; // 64 KB
|
|
@@ -79,6 +82,23 @@ class OnlineServer extends EventEmitter {
|
|
|
79
82
|
this.tlsKey = options.tlsKey || null;
|
|
80
83
|
}
|
|
81
84
|
|
|
85
|
+
parseRequestUrl(rawUrl) {
|
|
86
|
+
try {
|
|
87
|
+
return new URL(rawUrl || "/", "http://localhost");
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
corsHeaders(extra = {}) {
|
|
94
|
+
return {
|
|
95
|
+
"Access-Control-Allow-Origin": "*",
|
|
96
|
+
"Access-Control-Allow-Headers": "Authorization, Content-Type",
|
|
97
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
98
|
+
...extra,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
82
102
|
loadTokens(options) {
|
|
83
103
|
if (options.tokens) {
|
|
84
104
|
return new Set(Array.isArray(options.tokens) ? options.tokens : Object.keys(options.tokens));
|
|
@@ -117,27 +137,61 @@ class OnlineServer extends EventEmitter {
|
|
|
117
137
|
}
|
|
118
138
|
|
|
119
139
|
const requestHandler = (req, res) => {
|
|
120
|
-
|
|
121
|
-
|
|
140
|
+
const parsedUrl = this.parseRequestUrl(req.url);
|
|
141
|
+
if (!parsedUrl) {
|
|
142
|
+
res.writeHead(404, this.corsHeaders());
|
|
122
143
|
res.end();
|
|
123
144
|
return;
|
|
124
145
|
}
|
|
125
146
|
|
|
126
|
-
|
|
147
|
+
const pathname = parsedUrl.pathname || "/";
|
|
148
|
+
const publicMessagesMatch = pathname.match(/^\/ufoo\/online\/public\/channels\/([^/]+)\/messages$/);
|
|
149
|
+
const privateMessagesMatch = pathname.match(/^\/ufoo\/online\/channels\/([^/]+)\/messages$/);
|
|
150
|
+
|
|
151
|
+
if (req.method === "OPTIONS") {
|
|
152
|
+
res.writeHead(204, this.corsHeaders());
|
|
153
|
+
res.end();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (publicMessagesMatch) {
|
|
158
|
+
const channelRef = decodeURIComponent(publicMessagesMatch[1] || "");
|
|
159
|
+
this.handleChannelMessagesRequest(req, res, channelRef, parsedUrl);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (privateMessagesMatch) {
|
|
164
|
+
const channelRef = decodeURIComponent(privateMessagesMatch[1] || "");
|
|
165
|
+
if (!this.authenticateHttp(req, res)) return;
|
|
166
|
+
this.handleChannelMessagesRequest(req, res, channelRef, parsedUrl);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (pathname === "/ufoo/online/public/channels") {
|
|
171
|
+
this.handlePublicChannelsRequest(req, res, parsedUrl);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (pathname === "/ufoo/online/public/rooms") {
|
|
176
|
+
this.handlePublicRoomsRequest(req, res, parsedUrl);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (pathname.startsWith("/ufoo/online/rooms")) {
|
|
127
181
|
// Step 4: HTTP auth
|
|
128
182
|
if (!this.authenticateHttp(req, res)) return;
|
|
129
183
|
this.handleRoomsRequest(req, res);
|
|
130
184
|
return;
|
|
131
185
|
}
|
|
132
186
|
|
|
133
|
-
if (
|
|
187
|
+
if (pathname.startsWith("/ufoo/online/channels")) {
|
|
134
188
|
// Step 4: HTTP auth
|
|
135
189
|
if (!this.authenticateHttp(req, res)) return;
|
|
136
190
|
this.handleChannelsRequest(req, res);
|
|
137
191
|
return;
|
|
138
192
|
}
|
|
139
193
|
|
|
140
|
-
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
194
|
+
res.writeHead(200, this.corsHeaders({ "Content-Type": "text/plain" }));
|
|
141
195
|
res.end("ufoo-online: running\n");
|
|
142
196
|
};
|
|
143
197
|
|
|
@@ -258,7 +312,7 @@ class OnlineServer extends EventEmitter {
|
|
|
258
312
|
}
|
|
259
313
|
|
|
260
314
|
sendJson(res, statusCode, payload) {
|
|
261
|
-
res.writeHead(statusCode, { "Content-Type": "application/json" });
|
|
315
|
+
res.writeHead(statusCode, this.corsHeaders({ "Content-Type": "application/json" }));
|
|
262
316
|
res.end(JSON.stringify(payload));
|
|
263
317
|
}
|
|
264
318
|
|
|
@@ -286,17 +340,123 @@ class OnlineServer extends EventEmitter {
|
|
|
286
340
|
type: room.type,
|
|
287
341
|
members: room.members.size,
|
|
288
342
|
created_at: room.created_at,
|
|
343
|
+
created_by: room.created_by || "",
|
|
344
|
+
password_required: room.type === "private",
|
|
289
345
|
}));
|
|
290
346
|
}
|
|
291
347
|
|
|
292
348
|
listChannels() {
|
|
293
|
-
return Array.from(this.channels.entries()).map(([channelId, channel]) =>
|
|
349
|
+
return Array.from(this.channels.entries()).map(([channelId, channel]) => {
|
|
350
|
+
const history = this.channelMessageHistory.get(channelId) || [];
|
|
351
|
+
const last = history.length > 0 ? history[history.length - 1] : null;
|
|
352
|
+
return {
|
|
353
|
+
channel_id: channelId,
|
|
354
|
+
name: channel.name || "",
|
|
355
|
+
type: channel.type || "public",
|
|
356
|
+
members: channel.members.size,
|
|
357
|
+
created_at: channel.created_at,
|
|
358
|
+
created_by: channel.created_by || "",
|
|
359
|
+
message_count: history.length,
|
|
360
|
+
last_message_at: channel.last_message_at || (last ? last.ts : null),
|
|
361
|
+
};
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
listPublicRooms(type = "") {
|
|
366
|
+
return this.listRooms()
|
|
367
|
+
.filter((room) => !type || room.type === type)
|
|
368
|
+
.map((room) => ({
|
|
369
|
+
room_id: room.room_id,
|
|
370
|
+
name: room.name || "",
|
|
371
|
+
type: room.type,
|
|
372
|
+
created_at: room.created_at,
|
|
373
|
+
created_by: room.created_by || "",
|
|
374
|
+
password_required: room.password_required !== false,
|
|
375
|
+
}));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
listChannelMessages(channelRef, limit = 80) {
|
|
379
|
+
const resolved = this.resolveChannel(channelRef);
|
|
380
|
+
if (!resolved) return null;
|
|
381
|
+
const history = this.channelMessageHistory.get(resolved.channelId) || [];
|
|
382
|
+
const safeLimit = Number.isFinite(limit) ? Math.max(1, Math.min(limit, 500)) : 80;
|
|
383
|
+
const start = Math.max(0, history.length - safeLimit);
|
|
384
|
+
return {
|
|
385
|
+
channel_id: resolved.channelId,
|
|
386
|
+
name: resolved.channel.name || "",
|
|
387
|
+
type: resolved.channel.type || "public",
|
|
388
|
+
messages: history.slice(start),
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
handlePublicRoomsRequest(req, res, parsedUrl) {
|
|
393
|
+
if (req.method !== "GET") {
|
|
394
|
+
this.sendJson(res, 405, { ok: false, error: "Method not allowed" });
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const type = String(parsedUrl.searchParams.get("type") || "").trim();
|
|
398
|
+
this.sendJson(res, 200, { ok: true, rooms: this.listPublicRooms(type) });
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
handlePublicChannelsRequest(req, res, parsedUrl) {
|
|
402
|
+
if (req.method !== "GET") {
|
|
403
|
+
this.sendJson(res, 405, { ok: false, error: "Method not allowed" });
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
const type = String(parsedUrl.searchParams.get("type") || "").trim();
|
|
407
|
+
const channels = this.listChannels().filter((channel) => !type || channel.type === type);
|
|
408
|
+
this.sendJson(res, 200, { ok: true, channels });
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
handleChannelMessagesRequest(req, res, channelRef, parsedUrl) {
|
|
412
|
+
if (req.method !== "GET") {
|
|
413
|
+
this.sendJson(res, 405, { ok: false, error: "Method not allowed" });
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const limitRaw = Number.parseInt(String(parsedUrl.searchParams.get("limit") || "80"), 10);
|
|
417
|
+
const limit = Number.isFinite(limitRaw) ? limitRaw : 80;
|
|
418
|
+
const channelData = this.listChannelMessages(channelRef, limit);
|
|
419
|
+
if (!channelData) {
|
|
420
|
+
this.sendJson(res, 404, { ok: false, error: "Channel not found" });
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
this.sendJson(res, 200, {
|
|
424
|
+
ok: true,
|
|
425
|
+
channel: {
|
|
426
|
+
channel_id: channelData.channel_id,
|
|
427
|
+
name: channelData.name,
|
|
428
|
+
type: channelData.type,
|
|
429
|
+
},
|
|
430
|
+
messages: channelData.messages,
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
recordChannelMessage(channelId, channel, client, eventPayload) {
|
|
435
|
+
const rawText = eventPayload?.payload?.message;
|
|
436
|
+
let text = "";
|
|
437
|
+
if (typeof rawText === "string") text = rawText;
|
|
438
|
+
else if (rawText !== null && rawText !== undefined) text = JSON.stringify(rawText);
|
|
439
|
+
if (!text) return;
|
|
440
|
+
|
|
441
|
+
const history = this.channelMessageHistory.get(channelId) || [];
|
|
442
|
+
const ts = eventPayload.ts || new Date().toISOString();
|
|
443
|
+
const entry = {
|
|
444
|
+
event_id: `event_${String(++this.eventSeq).padStart(8, "0")}`,
|
|
445
|
+
ts,
|
|
294
446
|
channel_id: channelId,
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
447
|
+
channel_name: channel?.name || channelId,
|
|
448
|
+
from: client.subscriberId || "",
|
|
449
|
+
nickname: client.nickname || "",
|
|
450
|
+
text,
|
|
451
|
+
};
|
|
452
|
+
history.push(entry);
|
|
453
|
+
if (history.length > this.channelHistoryLimit) {
|
|
454
|
+
history.splice(0, history.length - this.channelHistoryLimit);
|
|
455
|
+
}
|
|
456
|
+
this.channelMessageHistory.set(channelId, history);
|
|
457
|
+
if (channel) {
|
|
458
|
+
channel.last_message_at = ts;
|
|
459
|
+
}
|
|
300
460
|
}
|
|
301
461
|
|
|
302
462
|
handleRoomsRequest(req, res) {
|
|
@@ -320,6 +480,7 @@ class OnlineServer extends EventEmitter {
|
|
|
320
480
|
}
|
|
321
481
|
const name = String(payload.name || "").trim();
|
|
322
482
|
const type = String(payload.type).trim();
|
|
483
|
+
const createdBy = String(payload.created_by || payload.creator || "").trim();
|
|
323
484
|
if (!["public", "private"].includes(type)) {
|
|
324
485
|
this.sendJson(res, 400, { ok: false, error: "Invalid room type" });
|
|
325
486
|
return;
|
|
@@ -328,6 +489,10 @@ class OnlineServer extends EventEmitter {
|
|
|
328
489
|
const nameErr = this.validateIdentifier(name, "name");
|
|
329
490
|
if (nameErr) { this.sendJson(res, 400, { ok: false, error: nameErr }); return; }
|
|
330
491
|
}
|
|
492
|
+
if (createdBy) {
|
|
493
|
+
const creatorErr = this.validateIdentifier(createdBy, "created_by");
|
|
494
|
+
if (creatorErr) { this.sendJson(res, 400, { ok: false, error: creatorErr }); return; }
|
|
495
|
+
}
|
|
331
496
|
if (this.rooms.size >= this.maxRooms) {
|
|
332
497
|
this.sendJson(res, 429, { ok: false, error: "Room limit reached" });
|
|
333
498
|
return;
|
|
@@ -354,8 +519,12 @@ class OnlineServer extends EventEmitter {
|
|
|
354
519
|
type,
|
|
355
520
|
members: new Set(),
|
|
356
521
|
created_at: new Date().toISOString(),
|
|
522
|
+
created_by: createdBy,
|
|
523
|
+
});
|
|
524
|
+
this.sendJson(res, 200, {
|
|
525
|
+
ok: true,
|
|
526
|
+
room: { room_id: roomId, name, type, created_by: createdBy, password_required: type === "private" },
|
|
357
527
|
});
|
|
358
|
-
this.sendJson(res, 200, { ok: true, room: { room_id: roomId, name, type } });
|
|
359
528
|
})
|
|
360
529
|
.catch(() => {
|
|
361
530
|
// Step 3: 413 on payload too large
|
|
@@ -388,12 +557,17 @@ class OnlineServer extends EventEmitter {
|
|
|
388
557
|
}
|
|
389
558
|
const name = String(payload.name || "").trim();
|
|
390
559
|
const type = String(payload.type || "public").trim();
|
|
560
|
+
const createdBy = String(payload.created_by || payload.creator || "").trim();
|
|
391
561
|
if (!name) {
|
|
392
562
|
this.sendJson(res, 400, { ok: false, error: "Invalid channel name" });
|
|
393
563
|
return;
|
|
394
564
|
}
|
|
395
565
|
const chNameErr = this.validateIdentifier(name, "name");
|
|
396
566
|
if (chNameErr) { this.sendJson(res, 400, { ok: false, error: chNameErr }); return; }
|
|
567
|
+
if (createdBy) {
|
|
568
|
+
const creatorErr = this.validateIdentifier(createdBy, "created_by");
|
|
569
|
+
if (creatorErr) { this.sendJson(res, 400, { ok: false, error: creatorErr }); return; }
|
|
570
|
+
}
|
|
397
571
|
if (!["world", "public"].includes(type)) {
|
|
398
572
|
this.sendJson(res, 400, { ok: false, error: "Invalid channel type" });
|
|
399
573
|
return;
|
|
@@ -420,9 +594,10 @@ class OnlineServer extends EventEmitter {
|
|
|
420
594
|
type,
|
|
421
595
|
members: new Set(),
|
|
422
596
|
created_at: new Date().toISOString(),
|
|
597
|
+
created_by: createdBy,
|
|
423
598
|
});
|
|
424
599
|
this.channelNames.set(name, channelId);
|
|
425
|
-
this.sendJson(res, 200, { ok: true, channel: { channel_id: channelId, name, type } });
|
|
600
|
+
this.sendJson(res, 200, { ok: true, channel: { channel_id: channelId, name, type, created_by: createdBy } });
|
|
426
601
|
})
|
|
427
602
|
.catch(() => {
|
|
428
603
|
// Step 3: 413 on payload too large
|
|
@@ -728,6 +903,7 @@ class OnlineServer extends EventEmitter {
|
|
|
728
903
|
type: "public",
|
|
729
904
|
members: new Set(),
|
|
730
905
|
created_at: new Date().toISOString(),
|
|
906
|
+
created_by: "",
|
|
731
907
|
};
|
|
732
908
|
this.channels.set(channelRef, channel);
|
|
733
909
|
this.channelNames.set(channelRef, channelRef);
|
|
@@ -877,6 +1053,9 @@ class OnlineServer extends EventEmitter {
|
|
|
877
1053
|
const members = channel ? channel.members : null;
|
|
878
1054
|
if (!members || members.size === 0) return;
|
|
879
1055
|
payload.channel = channelId;
|
|
1056
|
+
if (kind === "message") {
|
|
1057
|
+
this.recordChannelMessage(channelId, channel, client, payload);
|
|
1058
|
+
}
|
|
880
1059
|
members.forEach((member) => {
|
|
881
1060
|
if (member !== client) {
|
|
882
1061
|
this.send(member.ws, payload);
|
|
@@ -7,8 +7,13 @@ const IPC_REQUEST_TYPES = {
|
|
|
7
7
|
BUS_SEND: "bus_send",
|
|
8
8
|
CLOSE_AGENT: "close_agent",
|
|
9
9
|
LAUNCH_AGENT: "launch_agent",
|
|
10
|
+
LAUNCH_GROUP: "launch_group",
|
|
10
11
|
RESUME_AGENTS: "resume_agents",
|
|
11
12
|
LIST_RECOVERABLE_AGENTS: "list_recoverable_agents",
|
|
13
|
+
STOP_GROUP: "stop_group",
|
|
14
|
+
GROUP_STATUS: "group_status",
|
|
15
|
+
GROUP_TEMPLATE_VALIDATE: "group_template_validate",
|
|
16
|
+
GROUP_DIAGRAM: "group_diagram",
|
|
12
17
|
REGISTER_AGENT: "register_agent",
|
|
13
18
|
AGENT_READY: "agent_ready",
|
|
14
19
|
AGENT_REPORT: "agent_report",
|
package/src/ufoo/paths.js
CHANGED
|
@@ -17,6 +17,7 @@ function getUfooPaths(projectRoot) {
|
|
|
17
17
|
const busDaemonCountsDir = path.join(busDaemonDir, "counts");
|
|
18
18
|
|
|
19
19
|
const runDir = path.join(ufooDir, "run");
|
|
20
|
+
const groupsDir = path.join(ufooDir, "groups");
|
|
20
21
|
const ufooDaemonPid = path.join(runDir, "ufoo-daemon.pid");
|
|
21
22
|
const ufooDaemonLog = path.join(runDir, "ufoo-daemon.log");
|
|
22
23
|
const ufooSock = path.join(runDir, "ufoo.sock");
|
|
@@ -35,6 +36,7 @@ function getUfooPaths(projectRoot) {
|
|
|
35
36
|
busDaemonLog,
|
|
36
37
|
busDaemonCountsDir,
|
|
37
38
|
runDir,
|
|
39
|
+
groupsDir,
|
|
38
40
|
ufooDaemonPid,
|
|
39
41
|
ufooDaemonLog,
|
|
40
42
|
ufooSock,
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"template": {
|
|
4
|
+
"id": "software-dev-basic",
|
|
5
|
+
"alias": "dev-basic",
|
|
6
|
+
"name": "Software Dev Basic"
|
|
7
|
+
},
|
|
8
|
+
"defaults": {
|
|
9
|
+
"launch_mode": "auto",
|
|
10
|
+
"start_timeout_ms": 15000
|
|
11
|
+
},
|
|
12
|
+
"agents": [
|
|
13
|
+
{
|
|
14
|
+
"id": "pm",
|
|
15
|
+
"nickname": "pm",
|
|
16
|
+
"type": "codex",
|
|
17
|
+
"role": "task coordinator",
|
|
18
|
+
"prompt_profile": "task-breakdown",
|
|
19
|
+
"accept_from": [],
|
|
20
|
+
"report_to": [],
|
|
21
|
+
"startup_order": 1,
|
|
22
|
+
"depends_on": []
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"id": "architect",
|
|
26
|
+
"nickname": "architect",
|
|
27
|
+
"type": "claude",
|
|
28
|
+
"role": "system architect",
|
|
29
|
+
"prompt_profile": "architecture-review",
|
|
30
|
+
"accept_from": [
|
|
31
|
+
"pm"
|
|
32
|
+
],
|
|
33
|
+
"report_to": [
|
|
34
|
+
"pm"
|
|
35
|
+
],
|
|
36
|
+
"startup_order": 2,
|
|
37
|
+
"depends_on": [
|
|
38
|
+
"pm"
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"id": "builder",
|
|
43
|
+
"nickname": "builder",
|
|
44
|
+
"type": "codex",
|
|
45
|
+
"role": "implementation engineer",
|
|
46
|
+
"prompt_profile": "code-implement",
|
|
47
|
+
"accept_from": [
|
|
48
|
+
"pm",
|
|
49
|
+
"architect"
|
|
50
|
+
],
|
|
51
|
+
"report_to": [
|
|
52
|
+
"pm"
|
|
53
|
+
],
|
|
54
|
+
"startup_order": 3,
|
|
55
|
+
"depends_on": [
|
|
56
|
+
"pm",
|
|
57
|
+
"architect"
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
],
|
|
61
|
+
"edges": [
|
|
62
|
+
{
|
|
63
|
+
"from": "pm",
|
|
64
|
+
"to": "architect",
|
|
65
|
+
"kind": "task"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"from": "pm",
|
|
69
|
+
"to": "builder",
|
|
70
|
+
"kind": "task"
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"from": "architect",
|
|
74
|
+
"to": "builder",
|
|
75
|
+
"kind": "review"
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema_version": 1,
|
|
3
|
+
"template": {
|
|
4
|
+
"id": "research-quick",
|
|
5
|
+
"alias": "research-quick",
|
|
6
|
+
"name": "Research Quick"
|
|
7
|
+
},
|
|
8
|
+
"defaults": {
|
|
9
|
+
"launch_mode": "auto",
|
|
10
|
+
"start_timeout_ms": 10000
|
|
11
|
+
},
|
|
12
|
+
"agents": [
|
|
13
|
+
{
|
|
14
|
+
"id": "researcher",
|
|
15
|
+
"nickname": "researcher",
|
|
16
|
+
"type": "claude",
|
|
17
|
+
"role": "collect references and summarize findings",
|
|
18
|
+
"prompt_profile": "research-scan",
|
|
19
|
+
"accept_from": [],
|
|
20
|
+
"report_to": [],
|
|
21
|
+
"startup_order": 1,
|
|
22
|
+
"depends_on": []
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"id": "coder",
|
|
26
|
+
"nickname": "coder",
|
|
27
|
+
"type": "codex",
|
|
28
|
+
"role": "prototype and verify implementation",
|
|
29
|
+
"prompt_profile": "rapid-prototype",
|
|
30
|
+
"accept_from": [
|
|
31
|
+
"researcher"
|
|
32
|
+
],
|
|
33
|
+
"report_to": [
|
|
34
|
+
"researcher"
|
|
35
|
+
],
|
|
36
|
+
"startup_order": 2,
|
|
37
|
+
"depends_on": [
|
|
38
|
+
"researcher"
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
],
|
|
42
|
+
"edges": [
|
|
43
|
+
{
|
|
44
|
+
"from": "researcher",
|
|
45
|
+
"to": "coder",
|
|
46
|
+
"kind": "task"
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|