u-foo 1.7.5 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -1
- package/README.zh-CN.md +9 -1
- package/bin/ufoo.js +4 -2
- package/package.json +1 -1
- package/src/agent/cliRunner.js +3 -2
- package/src/agent/ucodeBootstrap.js +5 -3
- package/src/agent/ufooAgent.js +184 -5
- package/src/assistant/constants.js +1 -1
- package/src/chat/commandExecutor.js +98 -3
- package/src/chat/commands.js +7 -0
- package/src/chat/completionController.js +40 -0
- package/src/chat/daemonMessageRouter.js +21 -1
- package/src/chat/dashboardKeyController.js +55 -3
- package/src/chat/dashboardView.js +31 -5
- package/src/chat/index.js +152 -36
- package/src/chat/inputListenerController.js +14 -0
- package/src/chat/inputSubmitHandler.js +9 -5
- package/src/chat/transientAgentState.js +64 -0
- package/src/cli/groupCoreCommands.js +21 -12
- package/src/cli.js +23 -1
- package/src/daemon/groupOrchestrator.js +581 -97
- package/src/daemon/index.js +418 -3
- package/src/daemon/ops.js +25 -7
- package/src/daemon/promptLoop.js +16 -0
- package/src/daemon/promptRequest.js +126 -2
- package/src/daemon/reporting.js +18 -0
- package/src/daemon/soloBootstrap.js +435 -0
- package/src/daemon/status.js +5 -1
- package/src/globalMode.js +33 -0
- package/src/group/bootstrap.js +157 -0
- package/src/group/promptProfiles.js +646 -0
- package/src/group/templateValidation.js +99 -0
- package/src/group/validateTemplate.js +36 -5
- package/src/init/index.js +13 -7
- package/src/report/store.js +6 -0
- package/src/shared/eventContract.js +1 -0
- package/templates/groups/{dev-basic.json → build-lane.json} +38 -34
- package/templates/groups/product-discovery.json +79 -0
- package/templates/groups/ui-polish.json +87 -0
- package/templates/groups/verify-ship.json +79 -0
- package/templates/groups/research-quick.json +0 -49
|
@@ -2,41 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require("fs");
|
|
4
4
|
const path = require("path");
|
|
5
|
+
const EventBus = require("../bus");
|
|
6
|
+
const { prepareUcodeBootstrap } = require("../agent/ucodeBootstrap");
|
|
7
|
+
const { loadConfig } = require("../config");
|
|
8
|
+
const {
|
|
9
|
+
buildGroupPromptMetadata,
|
|
10
|
+
composeGroupBootstrapPrompt,
|
|
11
|
+
computeBootstrapFingerprint,
|
|
12
|
+
computeRosterVersion,
|
|
13
|
+
} = require("../group/bootstrap");
|
|
14
|
+
const { validateTemplateTarget: validateGroupTemplateTarget } = require("../group/templateValidation");
|
|
5
15
|
const { getUfooPaths } = require("../ufoo/paths");
|
|
6
|
-
const { resolveTemplateReference } = require("../group/templates");
|
|
7
|
-
const { validateTemplate } = require("../group/validateTemplate");
|
|
8
16
|
|
|
9
17
|
function asTrimmedString(value) {
|
|
10
18
|
if (typeof value !== "string") return "";
|
|
11
19
|
return value.trim();
|
|
12
20
|
}
|
|
13
21
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
if (!Array.isArray(errors) || errors.length === 0) return "";
|
|
18
|
-
return errors
|
|
19
|
-
.map((item) => `${item.filePath}: ${item.error}`)
|
|
20
|
-
.join("; ");
|
|
22
|
+
function asStringArray(value) {
|
|
23
|
+
if (!Array.isArray(value)) return [];
|
|
24
|
+
return value.map((item) => asTrimmedString(item)).filter(Boolean);
|
|
21
25
|
}
|
|
22
26
|
|
|
23
|
-
|
|
24
|
-
const resolved = resolveTemplateReference(projectRoot, target, {
|
|
25
|
-
allowPath: options.allowPath !== false,
|
|
26
|
-
cwd: options.cwd || projectRoot,
|
|
27
|
-
...(options.templatesOptions || {}),
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
if (resolved.entry) {
|
|
31
|
-
return { ok: true, resolved, error: "" };
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const details = formatResolveErrors(resolved.errors || []);
|
|
35
|
-
const error = details
|
|
36
|
-
? `failed to load template "${target}": ${details}`
|
|
37
|
-
: `template not found: ${target}`;
|
|
38
|
-
return { ok: false, resolved, error };
|
|
39
|
-
}
|
|
27
|
+
const SAFE_GROUP_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/;
|
|
40
28
|
|
|
41
29
|
function normalizeInstanceId(value = "") {
|
|
42
30
|
const text = asTrimmedString(value);
|
|
@@ -58,16 +46,19 @@ function buildLaunchPlan(templateDoc = {}) {
|
|
|
58
46
|
for (const agent of agents) {
|
|
59
47
|
const nickname = asTrimmedString(agent && agent.nickname);
|
|
60
48
|
if (!nickname) continue;
|
|
61
|
-
const dependsOn =
|
|
62
|
-
? agent.depends_on.map((item) => asTrimmedString(item)).filter(Boolean)
|
|
63
|
-
: [];
|
|
49
|
+
const dependsOn = asStringArray(agent.depends_on);
|
|
64
50
|
const startupOrder = Number.isInteger(agent.startup_order) ? agent.startup_order : 0;
|
|
65
51
|
remaining.set(nickname, {
|
|
66
52
|
id: asTrimmedString(agent.id),
|
|
67
53
|
nickname,
|
|
54
|
+
requested_type: asTrimmedString(agent.type),
|
|
68
55
|
type: asTrimmedString(agent.type),
|
|
56
|
+
role: asTrimmedString(agent.role),
|
|
57
|
+
prompt_profile: asTrimmedString(agent.prompt_profile),
|
|
69
58
|
startup_order: startupOrder,
|
|
70
59
|
depends_on: dependsOn,
|
|
60
|
+
accept_from: asStringArray(agent.accept_from),
|
|
61
|
+
report_to: asStringArray(agent.report_to),
|
|
71
62
|
});
|
|
72
63
|
}
|
|
73
64
|
|
|
@@ -113,6 +104,16 @@ function groupFilePath(projectRoot, groupId) {
|
|
|
113
104
|
return path.join(ensureGroupsDir(projectRoot), `${normalized}.json`);
|
|
114
105
|
}
|
|
115
106
|
|
|
107
|
+
function memberBootstrapFilePath(projectRoot, groupId, nickname) {
|
|
108
|
+
return path.join(
|
|
109
|
+
getUfooPaths(projectRoot).agentDir,
|
|
110
|
+
"ucode",
|
|
111
|
+
"groups",
|
|
112
|
+
groupId,
|
|
113
|
+
`${nickname}.bootstrap.md`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
116
117
|
function writeGroupState(projectRoot, runtime) {
|
|
117
118
|
const filePath = groupFilePath(projectRoot, runtime.group_id);
|
|
118
119
|
fs.writeFileSync(filePath, `${JSON.stringify(runtime, null, 2)}\n`, "utf8");
|
|
@@ -190,6 +191,10 @@ function nowIso() {
|
|
|
190
191
|
return new Date().toISOString();
|
|
191
192
|
}
|
|
192
193
|
|
|
194
|
+
function sleep(ms) {
|
|
195
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
196
|
+
}
|
|
197
|
+
|
|
193
198
|
function buildLaunchHostContext(params = {}) {
|
|
194
199
|
const hostInjectSock = asTrimmedString(params.host_inject_sock || params.hostInjectSock);
|
|
195
200
|
const hostDaemonSock = asTrimmedString(params.host_daemon_sock || params.hostDaemonSock);
|
|
@@ -208,11 +213,181 @@ function buildLaunchHostContext(params = {}) {
|
|
|
208
213
|
return context;
|
|
209
214
|
}
|
|
210
215
|
|
|
216
|
+
function buildRelationshipMaps(templateDoc = {}, plan = []) {
|
|
217
|
+
const upstream = new Map();
|
|
218
|
+
const downstream = new Map();
|
|
219
|
+
|
|
220
|
+
for (const item of plan) {
|
|
221
|
+
upstream.set(item.nickname, new Set([...item.depends_on, ...item.accept_from]));
|
|
222
|
+
downstream.set(item.nickname, new Set(item.report_to));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const edges = Array.isArray(templateDoc.edges) ? templateDoc.edges : [];
|
|
226
|
+
for (const edge of edges) {
|
|
227
|
+
const from = asTrimmedString(edge && edge.from);
|
|
228
|
+
const to = asTrimmedString(edge && edge.to);
|
|
229
|
+
if (!from || !to) continue;
|
|
230
|
+
if (!downstream.has(from)) downstream.set(from, new Set());
|
|
231
|
+
if (!upstream.has(to)) upstream.set(to, new Set());
|
|
232
|
+
downstream.get(from).add(to);
|
|
233
|
+
upstream.get(to).add(from);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const nicknameOrder = new Map(plan.map((item, index) => [item.nickname, index]));
|
|
237
|
+
const byNickname = new Map();
|
|
238
|
+
for (const item of plan) {
|
|
239
|
+
const orderedUpstream = Array.from(upstream.get(item.nickname) || [])
|
|
240
|
+
.filter((nickname) => nicknameOrder.has(nickname))
|
|
241
|
+
.sort((a, b) => nicknameOrder.get(a) - nicknameOrder.get(b));
|
|
242
|
+
const orderedDownstream = Array.from(downstream.get(item.nickname) || [])
|
|
243
|
+
.filter((nickname) => nicknameOrder.has(nickname))
|
|
244
|
+
.sort((a, b) => nicknameOrder.get(a) - nicknameOrder.get(b));
|
|
245
|
+
byNickname.set(item.nickname, {
|
|
246
|
+
upstream: orderedUpstream,
|
|
247
|
+
downstream: orderedDownstream,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
return byNickname;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function pickRosterMembers(roster = [], nicknames = []) {
|
|
254
|
+
const wanted = new Set(Array.isArray(nicknames) ? nicknames : []);
|
|
255
|
+
return roster.filter((item) => wanted.has(item.nickname));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function resolveAutoAgentType(projectRoot, requestedType) {
|
|
259
|
+
const normalizedRequested = asTrimmedString(requestedType);
|
|
260
|
+
if (normalizedRequested && normalizedRequested !== "auto") return normalizedRequested;
|
|
261
|
+
|
|
262
|
+
const provider = asTrimmedString(loadConfig(projectRoot).agentProvider);
|
|
263
|
+
if (provider === "claude-cli") return "claude";
|
|
264
|
+
if (provider === "ucode") return "ucode";
|
|
265
|
+
return "codex";
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function buildExecutionPlan({
|
|
269
|
+
projectRoot,
|
|
270
|
+
groupId,
|
|
271
|
+
templateEntry,
|
|
272
|
+
templateDoc,
|
|
273
|
+
plan,
|
|
274
|
+
promptRegistry,
|
|
275
|
+
promptProfiles,
|
|
276
|
+
}) {
|
|
277
|
+
const profileByNickname = new Map();
|
|
278
|
+
for (const item of promptProfiles || []) {
|
|
279
|
+
if (item && item.nickname) profileByNickname.set(item.nickname, item);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const registryById = promptRegistry && promptRegistry.byId ? promptRegistry.byId : new Map();
|
|
283
|
+
const relationships = buildRelationshipMaps(templateDoc, plan);
|
|
284
|
+
|
|
285
|
+
const roster = plan.map((item) => {
|
|
286
|
+
const resolvedType = resolveAutoAgentType(projectRoot, item.requested_type || item.type);
|
|
287
|
+
const profile = profileByNickname.get(item.nickname) || null;
|
|
288
|
+
return {
|
|
289
|
+
nickname: item.nickname,
|
|
290
|
+
requested_type: item.requested_type || item.type,
|
|
291
|
+
type: resolvedType,
|
|
292
|
+
role: item.role,
|
|
293
|
+
prompt_profile: item.prompt_profile,
|
|
294
|
+
resolved_profile: profile ? profile.resolved_profile : "",
|
|
295
|
+
display_name: profile ? profile.display_name : "",
|
|
296
|
+
short_name: profile ? profile.short_name : "",
|
|
297
|
+
profile_source: profile ? profile.profile_source : "",
|
|
298
|
+
deprecated: profile ? profile.deprecated === true : false,
|
|
299
|
+
depends_on: item.depends_on.slice(),
|
|
300
|
+
accept_from: item.accept_from.slice(),
|
|
301
|
+
report_to: item.report_to.slice(),
|
|
302
|
+
};
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
const rosterVersion = computeRosterVersion(roster);
|
|
306
|
+
const templateInfo = templateEntry && templateEntry.data && templateEntry.data.template
|
|
307
|
+
? templateEntry.data.template
|
|
308
|
+
: {};
|
|
309
|
+
|
|
310
|
+
const executionPlan = plan.map((item) => {
|
|
311
|
+
const resolvedType = resolveAutoAgentType(projectRoot, item.requested_type || item.type);
|
|
312
|
+
const profile = profileByNickname.get(item.nickname) || null;
|
|
313
|
+
const resolvedProfile = profile ? registryById.get(profile.resolved_profile) || null : null;
|
|
314
|
+
const relation = relationships.get(item.nickname) || { upstream: [], downstream: [] };
|
|
315
|
+
const upstream = pickRosterMembers(roster, relation.upstream);
|
|
316
|
+
const downstream = pickRosterMembers(roster, relation.downstream);
|
|
317
|
+
const metadata = buildGroupPromptMetadata({
|
|
318
|
+
groupId,
|
|
319
|
+
templateAlias: templateEntry.alias || asTrimmedString(templateInfo.alias),
|
|
320
|
+
templateName: asTrimmedString(templateInfo.name),
|
|
321
|
+
rosterVersion,
|
|
322
|
+
member: {
|
|
323
|
+
nickname: item.nickname,
|
|
324
|
+
role: item.role,
|
|
325
|
+
prompt_profile: item.prompt_profile,
|
|
326
|
+
resolved_profile: profile ? profile.resolved_profile : "",
|
|
327
|
+
depends_on: item.depends_on,
|
|
328
|
+
accept_from: item.accept_from,
|
|
329
|
+
report_to: item.report_to,
|
|
330
|
+
},
|
|
331
|
+
groupMembers: roster,
|
|
332
|
+
upstream,
|
|
333
|
+
downstream,
|
|
334
|
+
});
|
|
335
|
+
const bootstrapRequired = Boolean(resolvedProfile && resolvedProfile.prompt);
|
|
336
|
+
const bootstrapStrategy = !bootstrapRequired
|
|
337
|
+
? "none"
|
|
338
|
+
: (resolvedType === "ucode" ? "ucode-bootstrap-file" : "post-launch-inject");
|
|
339
|
+
const bootstrapPrompt = bootstrapRequired
|
|
340
|
+
? composeGroupBootstrapPrompt({
|
|
341
|
+
profilePrompt: resolvedProfile.prompt,
|
|
342
|
+
metadata,
|
|
343
|
+
})
|
|
344
|
+
: "";
|
|
345
|
+
const bootstrapFingerprint = bootstrapRequired
|
|
346
|
+
? computeBootstrapFingerprint({
|
|
347
|
+
groupId,
|
|
348
|
+
nickname: item.nickname,
|
|
349
|
+
resolvedProfile: profile ? profile.resolved_profile : "",
|
|
350
|
+
rosterVersion,
|
|
351
|
+
promptText: bootstrapPrompt,
|
|
352
|
+
metadata,
|
|
353
|
+
})
|
|
354
|
+
: "";
|
|
355
|
+
|
|
356
|
+
return {
|
|
357
|
+
...item,
|
|
358
|
+
requested_type: item.requested_type || item.type,
|
|
359
|
+
type: resolvedType,
|
|
360
|
+
resolved_profile: profile ? profile.resolved_profile : "",
|
|
361
|
+
display_name: profile ? profile.display_name : "",
|
|
362
|
+
short_name: profile ? profile.short_name : "",
|
|
363
|
+
profile_source: profile ? profile.profile_source : "",
|
|
364
|
+
deprecated: profile ? profile.deprecated === true : false,
|
|
365
|
+
bootstrap_required: bootstrapRequired,
|
|
366
|
+
bootstrap_strategy: bootstrapStrategy,
|
|
367
|
+
bootstrap_metadata: metadata,
|
|
368
|
+
bootstrap_prompt: bootstrapPrompt,
|
|
369
|
+
bootstrap_fingerprint: bootstrapFingerprint,
|
|
370
|
+
bootstrap_file: bootstrapStrategy === "ucode-bootstrap-file"
|
|
371
|
+
? memberBootstrapFilePath(projectRoot, groupId, item.nickname)
|
|
372
|
+
: "",
|
|
373
|
+
upstream: relation.upstream.slice(),
|
|
374
|
+
downstream: relation.downstream.slice(),
|
|
375
|
+
};
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
roster,
|
|
380
|
+
rosterVersion,
|
|
381
|
+
executionPlan,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
211
385
|
function buildDefaultRuntime({
|
|
212
386
|
groupId,
|
|
213
387
|
instance,
|
|
214
388
|
templateEntry,
|
|
215
389
|
plan,
|
|
390
|
+
rosterVersion,
|
|
216
391
|
}) {
|
|
217
392
|
const templateInfo = templateEntry && templateEntry.data && templateEntry.data.template
|
|
218
393
|
? templateEntry.data.template
|
|
@@ -228,6 +403,7 @@ function buildDefaultRuntime({
|
|
|
228
403
|
template_version: Number.isInteger(templateEntry.schemaVersion) ? templateEntry.schemaVersion : null,
|
|
229
404
|
template_source: templateEntry.source || "",
|
|
230
405
|
template_file: templateEntry.filePath || "",
|
|
406
|
+
roster_version: rosterVersion || "",
|
|
231
407
|
created_at: createdAt,
|
|
232
408
|
started_at: createdAt,
|
|
233
409
|
updated_at: createdAt,
|
|
@@ -236,9 +412,29 @@ function buildDefaultRuntime({
|
|
|
236
412
|
index: idx,
|
|
237
413
|
template_agent_id: item.id || "",
|
|
238
414
|
nickname: item.nickname,
|
|
415
|
+
requested_type: item.requested_type || item.type,
|
|
239
416
|
type: item.type,
|
|
417
|
+
role: item.role || "",
|
|
418
|
+
prompt_profile: item.prompt_profile || "",
|
|
419
|
+
resolved_profile: item.resolved_profile || "",
|
|
420
|
+
display_name: item.display_name || "",
|
|
421
|
+
short_name: item.short_name || "",
|
|
422
|
+
profile_source: item.profile_source || "",
|
|
423
|
+
profile_deprecated: item.deprecated === true,
|
|
240
424
|
startup_order: item.startup_order,
|
|
241
425
|
depends_on: item.depends_on.slice(),
|
|
426
|
+
accept_from: item.accept_from.slice(),
|
|
427
|
+
report_to: item.report_to.slice(),
|
|
428
|
+
upstream: item.upstream.slice(),
|
|
429
|
+
downstream: item.downstream.slice(),
|
|
430
|
+
bootstrap_required: item.bootstrap_required === true,
|
|
431
|
+
bootstrap_strategy: item.bootstrap_strategy || "none",
|
|
432
|
+
bootstrap_status: item.bootstrap_required ? "pending" : "skipped",
|
|
433
|
+
bootstrap_attempted_at: "",
|
|
434
|
+
bootstrap_error: "",
|
|
435
|
+
bootstrapped_subscriber_id: "",
|
|
436
|
+
bootstrap_fingerprint: item.bootstrap_fingerprint || "",
|
|
437
|
+
bootstrap_file: item.bootstrap_file || "",
|
|
242
438
|
status: "pending",
|
|
243
439
|
managed: true,
|
|
244
440
|
subscriber_id: "",
|
|
@@ -260,12 +456,158 @@ function pickLaunchSubscriber(projectRoot, launchResult, nickname) {
|
|
|
260
456
|
return resolveActiveSubscriberByNickname(projectRoot, nickname);
|
|
261
457
|
}
|
|
262
458
|
|
|
459
|
+
function findAppliedBootstrapRecord(projectRoot, subscriberId, fingerprint, currentGroupId = "") {
|
|
460
|
+
const targetSubscriber = asTrimmedString(subscriberId);
|
|
461
|
+
const targetFingerprint = asTrimmedString(fingerprint);
|
|
462
|
+
if (!targetSubscriber || !targetFingerprint) return null;
|
|
463
|
+
|
|
464
|
+
const groups = listGroupStates(projectRoot);
|
|
465
|
+
for (const runtime of groups) {
|
|
466
|
+
if (!runtime || runtime.group_id === currentGroupId) continue;
|
|
467
|
+
const members = Array.isArray(runtime.members) ? runtime.members : [];
|
|
468
|
+
for (const member of members) {
|
|
469
|
+
if (!member) continue;
|
|
470
|
+
if (asTrimmedString(member.bootstrapped_subscriber_id) !== targetSubscriber) continue;
|
|
471
|
+
if (asTrimmedString(member.bootstrap_fingerprint) !== targetFingerprint) continue;
|
|
472
|
+
if (asTrimmedString(member.bootstrap_status) !== "applied") continue;
|
|
473
|
+
return {
|
|
474
|
+
group_id: asTrimmedString(runtime.group_id),
|
|
475
|
+
nickname: asTrimmedString(member.nickname),
|
|
476
|
+
bootstrap_attempted_at: asTrimmedString(member.bootstrap_attempted_at),
|
|
477
|
+
bootstrapped_subscriber_id: targetSubscriber,
|
|
478
|
+
bootstrap_fingerprint: targetFingerprint,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function canReuseBootstrappedMember(member, item, subscriberId) {
|
|
486
|
+
return Boolean(
|
|
487
|
+
member
|
|
488
|
+
&& member.bootstrap_status === "applied"
|
|
489
|
+
&& asTrimmedString(member.bootstrapped_subscriber_id) === asTrimmedString(subscriberId)
|
|
490
|
+
&& asTrimmedString(member.bootstrap_fingerprint) === asTrimmedString(item.bootstrap_fingerprint)
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function readAgentMeta(projectRoot, subscriberId) {
|
|
495
|
+
const targetSubscriber = asTrimmedString(subscriberId);
|
|
496
|
+
if (!targetSubscriber) return null;
|
|
497
|
+
const agents = readBusAgents(projectRoot);
|
|
498
|
+
const meta = agents[targetSubscriber];
|
|
499
|
+
return meta && typeof meta === "object" ? meta : null;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function parseTimestampMs(value) {
|
|
503
|
+
const ms = Date.parse(String(value || ""));
|
|
504
|
+
return Number.isFinite(ms) ? ms : Number.NaN;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function isBootstrapReadyState(activityState = "") {
|
|
508
|
+
const normalized = asTrimmedString(activityState).toLowerCase();
|
|
509
|
+
return normalized === "ready"
|
|
510
|
+
|| normalized === "idle"
|
|
511
|
+
|| normalized === "waiting_input"
|
|
512
|
+
|| normalized === "blocked";
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async function waitForBootstrapReady(
|
|
516
|
+
projectRoot,
|
|
517
|
+
subscriberId,
|
|
518
|
+
{
|
|
519
|
+
timeoutMs = 15000,
|
|
520
|
+
retryDelayMs = 250,
|
|
521
|
+
protectionMs = 3000,
|
|
522
|
+
workingGraceMs = 10000,
|
|
523
|
+
} = {}
|
|
524
|
+
) {
|
|
525
|
+
const targetSubscriber = asTrimmedString(subscriberId);
|
|
526
|
+
if (!targetSubscriber) {
|
|
527
|
+
return { ok: false, error: "missing subscriber_id for bootstrap readiness" };
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const deadline = Date.now() + timeoutMs;
|
|
531
|
+
const startedAt = Date.now();
|
|
532
|
+
let lastState = "";
|
|
533
|
+
let lastStatus = "";
|
|
534
|
+
while (Date.now() < deadline) {
|
|
535
|
+
const meta = readAgentMeta(projectRoot, targetSubscriber);
|
|
536
|
+
const status = asTrimmedString(meta && meta.status).toLowerCase();
|
|
537
|
+
const activityState = asTrimmedString(meta && meta.activity_state);
|
|
538
|
+
lastStatus = status || lastStatus;
|
|
539
|
+
lastState = activityState || lastState;
|
|
540
|
+
|
|
541
|
+
if (!meta && lastStatus) {
|
|
542
|
+
return { ok: false, error: "agent disappeared before bootstrap" };
|
|
543
|
+
}
|
|
544
|
+
if (status && status !== "active") {
|
|
545
|
+
return { ok: false, error: `agent became ${status} before bootstrap` };
|
|
546
|
+
}
|
|
547
|
+
if (isBootstrapReadyState(activityState)) {
|
|
548
|
+
return { ok: true, activity_state: activityState.toLowerCase() };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
const elapsedMs = Date.now() - startedAt;
|
|
552
|
+
const normalizedState = activityState.toLowerCase();
|
|
553
|
+
if (normalizedState === "working" && elapsedMs >= protectionMs) {
|
|
554
|
+
const activitySinceMs = parseTimestampMs(meta && meta.activity_since);
|
|
555
|
+
const workingMs = Number.isFinite(activitySinceMs)
|
|
556
|
+
? Math.max(0, Date.now() - activitySinceMs)
|
|
557
|
+
: elapsedMs;
|
|
558
|
+
if (workingMs >= workingGraceMs) {
|
|
559
|
+
return {
|
|
560
|
+
ok: true,
|
|
561
|
+
activity_state: "working",
|
|
562
|
+
degraded: true,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
// eslint-disable-next-line no-await-in-loop
|
|
567
|
+
await sleep(retryDelayMs);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return {
|
|
571
|
+
ok: false,
|
|
572
|
+
error: lastState
|
|
573
|
+
? `agent not ready for bootstrap (last activity_state=${lastState})`
|
|
574
|
+
: "agent not ready for bootstrap",
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
async function injectBootstrapPrompt(bus, subscriberId, promptText, timeoutMs = 15000, retryDelayMs = 250) {
|
|
579
|
+
const deadline = Date.now() + timeoutMs;
|
|
580
|
+
let lastError = null;
|
|
581
|
+
while (Date.now() < deadline) {
|
|
582
|
+
try {
|
|
583
|
+
// eslint-disable-next-line no-await-in-loop
|
|
584
|
+
await bus.inject(subscriberId, promptText);
|
|
585
|
+
return { ok: true };
|
|
586
|
+
} catch (err) {
|
|
587
|
+
lastError = err;
|
|
588
|
+
// eslint-disable-next-line no-await-in-loop
|
|
589
|
+
await sleep(retryDelayMs);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return {
|
|
593
|
+
ok: false,
|
|
594
|
+
error: lastError && lastError.message
|
|
595
|
+
? lastError.message
|
|
596
|
+
: `bootstrap inject failed for ${subscriberId}`,
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
|
|
263
600
|
function createGroupOrchestrator(options = {}) {
|
|
264
601
|
const {
|
|
265
602
|
projectRoot,
|
|
266
603
|
handleOps,
|
|
267
604
|
processManager = null,
|
|
268
605
|
templatesOptions = {},
|
|
606
|
+
promptProfilesOptions = {},
|
|
607
|
+
bootstrapTimeoutMs = 15000,
|
|
608
|
+
bootstrapRetryDelayMs = 250,
|
|
609
|
+
bootstrapProtectionMs = 3000,
|
|
610
|
+
bootstrapWorkingGraceMs = 10000,
|
|
269
611
|
} = options;
|
|
270
612
|
|
|
271
613
|
if (!projectRoot) {
|
|
@@ -290,36 +632,54 @@ function createGroupOrchestrator(options = {}) {
|
|
|
290
632
|
}
|
|
291
633
|
|
|
292
634
|
function validateTemplateTarget(target, resolveOptions = {}) {
|
|
293
|
-
|
|
294
|
-
if (!rawTarget) {
|
|
295
|
-
return { ok: false, error: "template target is required", errors: [], entry: null };
|
|
296
|
-
}
|
|
297
|
-
const resolvedState = resolveTemplateTarget(projectRoot, rawTarget, {
|
|
635
|
+
return validateGroupTemplateTarget(projectRoot, target, {
|
|
298
636
|
templatesOptions,
|
|
637
|
+
promptProfilesOptions,
|
|
299
638
|
allowPath: resolveOptions.allowPath !== false,
|
|
300
639
|
});
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
async function rollbackMembers(runtime, rollbackTargets = []) {
|
|
643
|
+
for (let j = rollbackTargets.length - 1; j >= 0; j -= 1) {
|
|
644
|
+
const target = rollbackTargets[j];
|
|
645
|
+
// eslint-disable-next-line no-await-in-loop
|
|
646
|
+
const closeResults = await handleOps(projectRoot, [{ action: "close", agent_id: target.target }], processManager);
|
|
647
|
+
const closeResult = Array.isArray(closeResults)
|
|
648
|
+
? closeResults.find((entry) => entry && entry.action === "close")
|
|
649
|
+
: null;
|
|
650
|
+
const targetMember = runtime.members[target.memberIndex];
|
|
651
|
+
if (closeResult && closeResult.ok !== false) {
|
|
652
|
+
targetMember.status = "rolled_back";
|
|
653
|
+
targetMember.stop = { ok: true, reason: "rollback", at: nowIso() };
|
|
654
|
+
} else {
|
|
655
|
+
targetMember.status = "rollback_failed";
|
|
656
|
+
targetMember.stop = {
|
|
657
|
+
ok: false,
|
|
658
|
+
reason: "rollback",
|
|
659
|
+
at: nowIso(),
|
|
660
|
+
error: closeResult?.error || "close failed",
|
|
661
|
+
};
|
|
662
|
+
runtime.errors.push({
|
|
663
|
+
stage: "rollback",
|
|
664
|
+
nickname: targetMember.nickname,
|
|
665
|
+
error: closeResult?.error || "close failed",
|
|
666
|
+
});
|
|
667
|
+
}
|
|
317
668
|
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
async function failGroupLaunch(runtime, rollbackTargets, stage, nickname, error) {
|
|
672
|
+
runtime.status = "failed";
|
|
673
|
+
runtime.updated_at = nowIso();
|
|
674
|
+
runtime.errors.push({ stage, nickname, error });
|
|
675
|
+
await rollbackMembers(runtime, rollbackTargets);
|
|
676
|
+
writeGroupState(projectRoot, runtime);
|
|
318
677
|
return {
|
|
319
|
-
ok:
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
678
|
+
ok: false,
|
|
679
|
+
status: runtime.status,
|
|
680
|
+
group_id: runtime.group_id,
|
|
681
|
+
error,
|
|
682
|
+
group: runtime,
|
|
323
683
|
};
|
|
324
684
|
}
|
|
325
685
|
|
|
@@ -362,6 +722,16 @@ function createGroupOrchestrator(options = {}) {
|
|
|
362
722
|
};
|
|
363
723
|
}
|
|
364
724
|
|
|
725
|
+
const compiled = buildExecutionPlan({
|
|
726
|
+
projectRoot,
|
|
727
|
+
groupId,
|
|
728
|
+
templateEntry: validated.entry,
|
|
729
|
+
templateDoc: validated.entry.data,
|
|
730
|
+
plan,
|
|
731
|
+
promptRegistry: validated.promptRegistry,
|
|
732
|
+
promptProfiles: validated.promptProfiles,
|
|
733
|
+
});
|
|
734
|
+
|
|
365
735
|
if (dryRun) {
|
|
366
736
|
return {
|
|
367
737
|
ok: true,
|
|
@@ -369,11 +739,27 @@ function createGroupOrchestrator(options = {}) {
|
|
|
369
739
|
status: "dry_run",
|
|
370
740
|
group_id: groupId,
|
|
371
741
|
template_alias: validated.entry.alias,
|
|
372
|
-
|
|
742
|
+
roster_version: compiled.rosterVersion,
|
|
743
|
+
members: compiled.executionPlan.map((item) => ({
|
|
373
744
|
nickname: item.nickname,
|
|
374
745
|
type: item.type,
|
|
746
|
+
role: item.role,
|
|
375
747
|
startup_order: item.startup_order,
|
|
376
748
|
depends_on: item.depends_on.slice(),
|
|
749
|
+
accept_from: item.accept_from.slice(),
|
|
750
|
+
report_to: item.report_to.slice(),
|
|
751
|
+
prompt_profile: item.prompt_profile,
|
|
752
|
+
resolved_profile: item.resolved_profile,
|
|
753
|
+
display_name: item.display_name,
|
|
754
|
+
short_name: item.short_name,
|
|
755
|
+
profile_source: item.profile_source,
|
|
756
|
+
deprecated: item.deprecated,
|
|
757
|
+
bootstrap_required: item.bootstrap_required,
|
|
758
|
+
bootstrap_strategy: item.bootstrap_strategy,
|
|
759
|
+
bootstrap_fingerprint: item.bootstrap_fingerprint,
|
|
760
|
+
upstream: item.upstream.slice(),
|
|
761
|
+
downstream: item.downstream.slice(),
|
|
762
|
+
group_members: compiled.roster,
|
|
377
763
|
})),
|
|
378
764
|
};
|
|
379
765
|
}
|
|
@@ -382,15 +768,43 @@ function createGroupOrchestrator(options = {}) {
|
|
|
382
768
|
groupId,
|
|
383
769
|
instance,
|
|
384
770
|
templateEntry: validated.entry,
|
|
385
|
-
plan,
|
|
771
|
+
plan: compiled.executionPlan,
|
|
772
|
+
rosterVersion: compiled.rosterVersion,
|
|
386
773
|
});
|
|
387
774
|
writeGroupState(projectRoot, runtime);
|
|
388
775
|
|
|
389
776
|
const rollbackTargets = [];
|
|
777
|
+
const eventBus = new EventBus(projectRoot);
|
|
390
778
|
|
|
391
|
-
for (let i = 0; i <
|
|
392
|
-
const item =
|
|
779
|
+
for (let i = 0; i < compiled.executionPlan.length; i += 1) {
|
|
780
|
+
const item = compiled.executionPlan[i];
|
|
393
781
|
const member = runtime.members[i];
|
|
782
|
+
const extraEnv = {};
|
|
783
|
+
|
|
784
|
+
if (item.bootstrap_strategy === "ucode-bootstrap-file") {
|
|
785
|
+
member.bootstrap_attempted_at = nowIso();
|
|
786
|
+
member.bootstrap_error = "";
|
|
787
|
+
try {
|
|
788
|
+
prepareUcodeBootstrap({
|
|
789
|
+
projectRoot,
|
|
790
|
+
targetFile: item.bootstrap_file,
|
|
791
|
+
promptText: item.bootstrap_prompt,
|
|
792
|
+
});
|
|
793
|
+
extraEnv.UFOO_UCODE_BOOTSTRAP_FILE = item.bootstrap_file;
|
|
794
|
+
} catch (err) {
|
|
795
|
+
member.status = "failed";
|
|
796
|
+
member.bootstrap_status = "failed";
|
|
797
|
+
member.bootstrap_error = err && err.message ? err.message : "failed to prepare ucode bootstrap";
|
|
798
|
+
return failGroupLaunch(
|
|
799
|
+
runtime,
|
|
800
|
+
rollbackTargets,
|
|
801
|
+
"bootstrap",
|
|
802
|
+
item.nickname,
|
|
803
|
+
member.bootstrap_error
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
394
808
|
const op = {
|
|
395
809
|
action: "launch",
|
|
396
810
|
agent: item.type,
|
|
@@ -398,6 +812,9 @@ function createGroupOrchestrator(options = {}) {
|
|
|
398
812
|
nickname: item.nickname,
|
|
399
813
|
...launchHostContext,
|
|
400
814
|
};
|
|
815
|
+
if (Object.keys(extraEnv).length > 0) {
|
|
816
|
+
op.extra_env = extraEnv;
|
|
817
|
+
}
|
|
401
818
|
|
|
402
819
|
// eslint-disable-next-line no-await-in-loop
|
|
403
820
|
const opsResults = await handleOps(projectRoot, [op], processManager);
|
|
@@ -408,46 +825,15 @@ function createGroupOrchestrator(options = {}) {
|
|
|
408
825
|
if (!launchResult || launchResult.ok === false) {
|
|
409
826
|
member.status = "failed";
|
|
410
827
|
member.launch = launchResult || {};
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
828
|
+
return failGroupLaunch(
|
|
829
|
+
runtime,
|
|
830
|
+
rollbackTargets,
|
|
831
|
+
"launch",
|
|
832
|
+
item.nickname,
|
|
833
|
+
launchResult && launchResult.error
|
|
417
834
|
? launchResult.error
|
|
418
|
-
: `launch failed for ${item.nickname}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
for (let j = rollbackTargets.length - 1; j >= 0; j -= 1) {
|
|
422
|
-
const target = rollbackTargets[j];
|
|
423
|
-
// eslint-disable-next-line no-await-in-loop
|
|
424
|
-
const closeResults = await handleOps(projectRoot, [{ action: "close", agent_id: target.target }], processManager);
|
|
425
|
-
const closeResult = Array.isArray(closeResults)
|
|
426
|
-
? closeResults.find((entry) => entry && entry.action === "close")
|
|
427
|
-
: null;
|
|
428
|
-
const targetMember = runtime.members[target.memberIndex];
|
|
429
|
-
if (closeResult && closeResult.ok !== false) {
|
|
430
|
-
targetMember.status = "rolled_back";
|
|
431
|
-
targetMember.stop = { ok: true, reason: "rollback", at: nowIso() };
|
|
432
|
-
} else {
|
|
433
|
-
targetMember.status = "rollback_failed";
|
|
434
|
-
targetMember.stop = { ok: false, reason: "rollback", at: nowIso(), error: closeResult?.error || "close failed" };
|
|
435
|
-
runtime.errors.push({
|
|
436
|
-
stage: "rollback",
|
|
437
|
-
nickname: targetMember.nickname,
|
|
438
|
-
error: closeResult?.error || "close failed",
|
|
439
|
-
});
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
writeGroupState(projectRoot, runtime);
|
|
444
|
-
return {
|
|
445
|
-
ok: false,
|
|
446
|
-
status: runtime.status,
|
|
447
|
-
group_id: runtime.group_id,
|
|
448
|
-
error: runtime.errors[runtime.errors.length - 1]?.error || "group launch failed",
|
|
449
|
-
group: runtime,
|
|
450
|
-
};
|
|
835
|
+
: `launch failed for ${item.nickname}`
|
|
836
|
+
);
|
|
451
837
|
}
|
|
452
838
|
|
|
453
839
|
const reused = Boolean(launchResult.skipped);
|
|
@@ -457,6 +843,7 @@ function createGroupOrchestrator(options = {}) {
|
|
|
457
843
|
member.subscriber_id = subscriberId || "";
|
|
458
844
|
member.launched_at = nowIso();
|
|
459
845
|
member.launch = launchResult;
|
|
846
|
+
member.bootstrap_error = "";
|
|
460
847
|
runtime.updated_at = nowIso();
|
|
461
848
|
|
|
462
849
|
if (!reused) {
|
|
@@ -464,8 +851,105 @@ function createGroupOrchestrator(options = {}) {
|
|
|
464
851
|
memberIndex: i,
|
|
465
852
|
target: subscriberId || item.nickname,
|
|
466
853
|
});
|
|
854
|
+
} else if (!canReuseBootstrappedMember(member, item, subscriberId)) {
|
|
855
|
+
const priorBootstrap = findAppliedBootstrapRecord(
|
|
856
|
+
projectRoot,
|
|
857
|
+
subscriberId,
|
|
858
|
+
item.bootstrap_fingerprint,
|
|
859
|
+
runtime.group_id
|
|
860
|
+
);
|
|
861
|
+
if (priorBootstrap) {
|
|
862
|
+
member.bootstrap_status = "applied";
|
|
863
|
+
member.bootstrap_attempted_at = priorBootstrap.bootstrap_attempted_at || nowIso();
|
|
864
|
+
member.bootstrapped_subscriber_id = priorBootstrap.bootstrapped_subscriber_id;
|
|
865
|
+
member.bootstrap_fingerprint = priorBootstrap.bootstrap_fingerprint;
|
|
866
|
+
member.bootstrap_error = "";
|
|
867
|
+
} else if (item.bootstrap_required && item.bootstrap_strategy === "post-launch-inject" && subscriberId) {
|
|
868
|
+
member.bootstrap_status = "pending";
|
|
869
|
+
} else {
|
|
870
|
+
member.status = "failed";
|
|
871
|
+
member.bootstrap_status = "failed";
|
|
872
|
+
member.bootstrap_error = `unsafe reused member "${item.nickname}": missing matching bootstrap fingerprint`;
|
|
873
|
+
writeGroupState(projectRoot, runtime);
|
|
874
|
+
return failGroupLaunch(
|
|
875
|
+
runtime,
|
|
876
|
+
rollbackTargets,
|
|
877
|
+
"bootstrap",
|
|
878
|
+
item.nickname,
|
|
879
|
+
member.bootstrap_error
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
if (!item.bootstrap_required) {
|
|
885
|
+
member.bootstrap_status = "skipped";
|
|
886
|
+
writeGroupState(projectRoot, runtime);
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
if (
|
|
891
|
+
member.bootstrap_status === "applied"
|
|
892
|
+
&& asTrimmedString(member.bootstrapped_subscriber_id) === asTrimmedString(subscriberId)
|
|
893
|
+
&& asTrimmedString(member.bootstrap_fingerprint) === asTrimmedString(item.bootstrap_fingerprint)
|
|
894
|
+
) {
|
|
895
|
+
writeGroupState(projectRoot, runtime);
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
member.bootstrap_attempted_at = member.bootstrap_attempted_at || nowIso();
|
|
900
|
+
if (item.bootstrap_strategy === "post-launch-inject") {
|
|
901
|
+
// Wait for the agent wrapper/startup sequence to settle before injecting
|
|
902
|
+
// the group bootstrap prompt, otherwise the default startup command flow
|
|
903
|
+
// can be interrupted mid-boot.
|
|
904
|
+
// eslint-disable-next-line no-await-in-loop
|
|
905
|
+
const ready = await waitForBootstrapReady(
|
|
906
|
+
projectRoot,
|
|
907
|
+
subscriberId,
|
|
908
|
+
{
|
|
909
|
+
timeoutMs: bootstrapTimeoutMs,
|
|
910
|
+
retryDelayMs: bootstrapRetryDelayMs,
|
|
911
|
+
protectionMs: bootstrapProtectionMs,
|
|
912
|
+
workingGraceMs: bootstrapWorkingGraceMs,
|
|
913
|
+
}
|
|
914
|
+
);
|
|
915
|
+
if (!ready.ok) {
|
|
916
|
+
member.status = "failed";
|
|
917
|
+
member.bootstrap_status = "failed";
|
|
918
|
+
member.bootstrap_error = ready.error || `bootstrap readiness failed for ${item.nickname}`;
|
|
919
|
+
return failGroupLaunch(
|
|
920
|
+
runtime,
|
|
921
|
+
rollbackTargets,
|
|
922
|
+
"bootstrap",
|
|
923
|
+
item.nickname,
|
|
924
|
+
member.bootstrap_error
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
// eslint-disable-next-line no-await-in-loop
|
|
928
|
+
const injected = await injectBootstrapPrompt(
|
|
929
|
+
eventBus,
|
|
930
|
+
subscriberId,
|
|
931
|
+
item.bootstrap_prompt,
|
|
932
|
+
bootstrapTimeoutMs,
|
|
933
|
+
bootstrapRetryDelayMs
|
|
934
|
+
);
|
|
935
|
+
if (!injected.ok) {
|
|
936
|
+
member.status = "failed";
|
|
937
|
+
member.bootstrap_status = "failed";
|
|
938
|
+
member.bootstrap_error = injected.error || `bootstrap inject failed for ${item.nickname}`;
|
|
939
|
+
return failGroupLaunch(
|
|
940
|
+
runtime,
|
|
941
|
+
rollbackTargets,
|
|
942
|
+
"bootstrap",
|
|
943
|
+
item.nickname,
|
|
944
|
+
member.bootstrap_error
|
|
945
|
+
);
|
|
946
|
+
}
|
|
467
947
|
}
|
|
468
948
|
|
|
949
|
+
member.bootstrap_status = "applied";
|
|
950
|
+
member.bootstrapped_subscriber_id = subscriberId || "";
|
|
951
|
+
member.bootstrap_fingerprint = item.bootstrap_fingerprint || "";
|
|
952
|
+
member.bootstrap_error = "";
|
|
469
953
|
writeGroupState(projectRoot, runtime);
|
|
470
954
|
}
|
|
471
955
|
|