u-foo 1.8.9 → 1.9.1
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/bin/uclaude.js +12 -2
- package/bin/ucodex.js +12 -2
- package/bin/ufoo.js +1 -1
- package/package.json +1 -1
- package/src/agent/defaultBootstrap.js +144 -0
- package/src/agent/launcher.js +45 -31
- package/src/agent/ufooAgent.js +1 -2
- package/src/bus/daemon.js +26 -1
- package/src/bus/index.js +23 -3
- package/src/chat/commandExecutor.js +103 -4
- package/src/chat/commands.js +20 -2
- package/src/chat/completionController.js +43 -4
- package/src/chat/daemonReconnect.js +6 -5
- package/src/chat/daemonTransport.js +7 -1
- package/src/chat/index.js +13 -5
- package/src/cli.js +191 -0
- package/src/daemon/groupOrchestrator.js +7 -0
- package/src/daemon/index.js +64 -39
- package/src/daemon/promptRequest.js +1 -1
- package/src/group/bootstrap.js +14 -0
- package/src/history/inputTimeline.js +601 -0
- package/src/{globalMode.js → projects/identity.js} +1 -1
- package/src/projects/index.js +11 -0
- package/src/solo/commands.js +31 -0
- package/src/ufoo/paths.js +2 -0
- package/templates/groups/build-ultra.json +8 -2
- /package/src/{chat/projectRuntimes.js → projects/runtimes.js} +0 -0
|
@@ -4,6 +4,13 @@ const FALLBACK_LAUNCH_SUBCOMMANDS = [
|
|
|
4
4
|
{ cmd: "ucode", desc: "Launch ucode core agent" },
|
|
5
5
|
];
|
|
6
6
|
|
|
7
|
+
function sortSubcommandEntries(a, b) {
|
|
8
|
+
const aOrder = Number.isFinite(a && a.order) ? a.order : Number.POSITIVE_INFINITY;
|
|
9
|
+
const bOrder = Number.isFinite(b && b.order) ? b.order : Number.POSITIVE_INFINITY;
|
|
10
|
+
if (aOrder !== bOrder) return aOrder - bOrder;
|
|
11
|
+
return String(a && a.cmd ? a.cmd : "").localeCompare(String(b && b.cmd ? b.cmd : ""), "en", { sensitivity: "base" });
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
function createCompletionController(options = {}) {
|
|
8
15
|
const {
|
|
9
16
|
input,
|
|
@@ -12,6 +19,7 @@ function createCompletionController(options = {}) {
|
|
|
12
19
|
promptBox,
|
|
13
20
|
commandRegistry = [],
|
|
14
21
|
getGroupTemplateCandidates = () => [],
|
|
22
|
+
getSoloProfileCandidates = () => [],
|
|
15
23
|
getMentionCandidates = () => [],
|
|
16
24
|
normalizeCommandPrefix = () => {},
|
|
17
25
|
truncateText = (text) => String(text || ""),
|
|
@@ -143,6 +151,7 @@ function createCompletionController(options = {}) {
|
|
|
143
151
|
const mainCmd = parts[0];
|
|
144
152
|
const isLaunch = mainCmd && mainCmd.toLowerCase() === "/launch";
|
|
145
153
|
const isGroup = mainCmd && mainCmd.toLowerCase() === "/group";
|
|
154
|
+
const isSolo = mainCmd && mainCmd.toLowerCase() === "/solo";
|
|
146
155
|
const wantsSubcommands = (parts.length > 1 || (endsWithSpace && parts.length === 1));
|
|
147
156
|
|
|
148
157
|
if (isGroup) {
|
|
@@ -169,6 +178,28 @@ function createCompletionController(options = {}) {
|
|
|
169
178
|
}
|
|
170
179
|
}
|
|
171
180
|
|
|
181
|
+
if (isSolo) {
|
|
182
|
+
const soloSubcommand = String(parts[1] || "").trim().toLowerCase();
|
|
183
|
+
const wantsSoloRunArgs = soloSubcommand === "run" && (parts.length > 2 || endsWithSpace);
|
|
184
|
+
if (wantsSoloRunArgs) {
|
|
185
|
+
const profileFilter = String(parts[2] || "").trim().toLowerCase();
|
|
186
|
+
return (Array.isArray(getSoloProfileCandidates()) ? getSoloProfileCandidates() : [])
|
|
187
|
+
.map((item) => {
|
|
188
|
+
const profileId = String(item && item.cmd ? item.cmd : item && item.id ? item.id : "").trim();
|
|
189
|
+
if (!profileId) return null;
|
|
190
|
+
const desc = String(item && item.desc ? item.desc : item && item.summary ? item.summary : "").trim();
|
|
191
|
+
return {
|
|
192
|
+
cmd: profileId,
|
|
193
|
+
desc,
|
|
194
|
+
isArgumentSuggestion: true,
|
|
195
|
+
argumentPrefix: "/solo run",
|
|
196
|
+
};
|
|
197
|
+
})
|
|
198
|
+
.filter((item) => item && (!profileFilter || item.cmd.toLowerCase().startsWith(profileFilter)))
|
|
199
|
+
.sort((a, b) => a.cmd.localeCompare(b.cmd, "en", { sensitivity: "base" }));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
172
203
|
if ((wantsSubcommands || isLaunch) && mainCmd && mainCmd.startsWith("/")) {
|
|
173
204
|
const subFilter = parts[1] || "";
|
|
174
205
|
const mainCmdObj = commandRegistry.find((item) =>
|
|
@@ -188,12 +219,12 @@ function createCompletionController(options = {}) {
|
|
|
188
219
|
if (isLaunch) {
|
|
189
220
|
return subs
|
|
190
221
|
.map((sub) => ({ ...sub, isSubcommand: true, parentCmd: mainCmd }))
|
|
191
|
-
.sort(
|
|
222
|
+
.sort(sortSubcommandEntries);
|
|
192
223
|
}
|
|
193
224
|
return subs
|
|
194
225
|
.filter((sub) => sub.cmd.toLowerCase().startsWith(subFilter.toLowerCase()))
|
|
195
226
|
.map((sub) => ({ ...sub, isSubcommand: true, parentCmd: mainCmd }))
|
|
196
|
-
.sort(
|
|
227
|
+
.sort(sortSubcommandEntries);
|
|
197
228
|
}
|
|
198
229
|
return [];
|
|
199
230
|
}
|
|
@@ -340,7 +371,11 @@ function createCompletionController(options = {}) {
|
|
|
340
371
|
|
|
341
372
|
if (!selected.isSubcommand && selected.subcommands && selected.subcommands.length > 0) {
|
|
342
373
|
show(input.value);
|
|
343
|
-
} else if (
|
|
374
|
+
} else if (
|
|
375
|
+
selected.isSubcommand
|
|
376
|
+
&& ((selected.parentCmd === "/group" && selected.cmd === "run")
|
|
377
|
+
|| (selected.parentCmd === "/solo" && selected.cmd === "run"))
|
|
378
|
+
) {
|
|
344
379
|
show(input.value);
|
|
345
380
|
} else {
|
|
346
381
|
hide();
|
|
@@ -384,7 +419,11 @@ function createCompletionController(options = {}) {
|
|
|
384
419
|
applyPreview(nextPreview);
|
|
385
420
|
if (!selected.isSubcommand && selected.subcommands && selected.subcommands.length > 0) {
|
|
386
421
|
show(input.value);
|
|
387
|
-
} else if (
|
|
422
|
+
} else if (
|
|
423
|
+
selected.isSubcommand
|
|
424
|
+
&& ((selected.parentCmd === "/group" && selected.cmd === "run")
|
|
425
|
+
|| (selected.parentCmd === "/solo" && selected.cmd === "run"))
|
|
426
|
+
) {
|
|
388
427
|
show(input.value);
|
|
389
428
|
} else {
|
|
390
429
|
hide();
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { restartLocks } = require("./daemonTransport");
|
|
2
|
+
|
|
1
3
|
function resolveDaemonConnection(daemonConnection) {
|
|
2
4
|
return typeof daemonConnection === "function" ? daemonConnection() : daemonConnection;
|
|
3
5
|
}
|
|
@@ -14,11 +16,10 @@ function restartDaemonFlow(options = {}) {
|
|
|
14
16
|
|
|
15
17
|
const statusMsg = resolveStatusLine || ((text) => logMessage("status", text));
|
|
16
18
|
|
|
17
|
-
let restartInProgress = false;
|
|
18
|
-
|
|
19
19
|
return async function restartDaemon() {
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
// Use global restart lock to prevent concurrent restart flows
|
|
21
|
+
if (restartLocks.get(projectRoot)) return;
|
|
22
|
+
restartLocks.set(projectRoot, true);
|
|
22
23
|
statusMsg("{gray-fg}⚙{/gray-fg} Restarting daemon...");
|
|
23
24
|
try {
|
|
24
25
|
const connection = resolveDaemonConnection(daemonConnection);
|
|
@@ -34,7 +35,7 @@ function restartDaemonFlow(options = {}) {
|
|
|
34
35
|
statusMsg("{gray-fg}✗{/gray-fg} Failed to reconnect to daemon");
|
|
35
36
|
}
|
|
36
37
|
} finally {
|
|
37
|
-
|
|
38
|
+
restartLocks.delete(projectRoot);
|
|
38
39
|
}
|
|
39
40
|
};
|
|
40
41
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
const { DAEMON_TRANSPORT_DEFAULTS } = require("./daemonTransportDefaults");
|
|
2
2
|
|
|
3
|
+
// Global restart lock per project to prevent concurrent restart flows
|
|
4
|
+
const restartLocks = new Map();
|
|
5
|
+
|
|
3
6
|
function createDaemonTransport(options = {}) {
|
|
4
7
|
const {
|
|
5
8
|
projectRoot,
|
|
@@ -34,7 +37,9 @@ function createDaemonTransport(options = {}) {
|
|
|
34
37
|
);
|
|
35
38
|
if (!client) {
|
|
36
39
|
// Retry once with a fresh daemon start and longer wait.
|
|
37
|
-
if
|
|
40
|
+
// Check if a restart is already in progress via the explicit restart flow.
|
|
41
|
+
const isExplicitRestartInProgress = restartLocks.get(target.projectRoot);
|
|
42
|
+
if (!isExplicitRestartInProgress && !isRunning(target.projectRoot)) {
|
|
38
43
|
startDaemon(target.projectRoot);
|
|
39
44
|
await new Promise((resolve) => setTimeout(resolve, restartDelayMs));
|
|
40
45
|
}
|
|
@@ -75,4 +80,5 @@ function createDaemonTransport(options = {}) {
|
|
|
75
80
|
|
|
76
81
|
module.exports = {
|
|
77
82
|
createDaemonTransport,
|
|
83
|
+
restartLocks,
|
|
78
84
|
};
|
package/src/chat/index.js
CHANGED
|
@@ -18,6 +18,7 @@ const { getUfooPaths } = require("../ufoo/paths");
|
|
|
18
18
|
const { startDaemon, stopDaemon, connectWithRetry } = require("./transport");
|
|
19
19
|
const { escapeBlessed, stripBlessedTags, truncateText } = require("./text");
|
|
20
20
|
const { COMMAND_REGISTRY, parseCommand, parseAtTarget } = require("./commands");
|
|
21
|
+
const { buildPromptProfileCandidates } = require("../solo/commands");
|
|
21
22
|
const inputMath = require("./inputMath");
|
|
22
23
|
const { createStreamTracker } = require("./streamTracker");
|
|
23
24
|
const agentDirectory = require("./agentDirectory");
|
|
@@ -43,15 +44,18 @@ const { createDaemonCoordinator } = require("./daemonCoordinator");
|
|
|
43
44
|
const { IPC_REQUEST_TYPES } = require("../shared/eventContract");
|
|
44
45
|
const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
|
|
45
46
|
const { createDaemonTransport } = require("./daemonTransport");
|
|
46
|
-
const { listProjectRuntimes, resolveRuntimeDir } = require("../projects/registry");
|
|
47
|
-
const { canonicalProjectRoot, buildProjectId } = require("../projects/projectId");
|
|
48
|
-
const { loadTemplateRegistry } = require("../group/templates");
|
|
49
47
|
const {
|
|
48
|
+
listProjectRuntimes,
|
|
49
|
+
resolveRuntimeDir,
|
|
50
|
+
canonicalProjectRoot,
|
|
51
|
+
buildProjectId,
|
|
50
52
|
sortProjectRuntimes,
|
|
51
53
|
parseTimestampMs,
|
|
52
54
|
filterVisibleProjectRuntimes,
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
isGlobalControllerProjectRoot,
|
|
56
|
+
resolveGlobalControllerProjectRoot,
|
|
57
|
+
} = require("../projects");
|
|
58
|
+
const { loadTemplateRegistry } = require("../group/templates");
|
|
55
59
|
const {
|
|
56
60
|
DEFAULT_TRANSIENT_AGENT_STATE_TTL_MS,
|
|
57
61
|
setTransientAgentState: setTransientAgentStateValue,
|
|
@@ -559,6 +563,10 @@ async function runChat(projectRoot, options = {}) {
|
|
|
559
563
|
source: item.source || "",
|
|
560
564
|
}));
|
|
561
565
|
},
|
|
566
|
+
getSoloProfileCandidates: () => {
|
|
567
|
+
const registry = loadPromptProfileRegistry(activeProjectRoot);
|
|
568
|
+
return buildPromptProfileCandidates(registry);
|
|
569
|
+
},
|
|
562
570
|
getMentionCandidates: () => activeAgents.map((id) => ({
|
|
563
571
|
id,
|
|
564
572
|
label: getAgentLabel(id),
|
package/src/cli.js
CHANGED
|
@@ -7,6 +7,9 @@ const { runBusCoreCommand } = require("./cli/busCoreCommands");
|
|
|
7
7
|
const { runCtxCommand } = require("./cli/ctxCoreCommands");
|
|
8
8
|
const { runOnlineCommand } = require("./cli/onlineCoreCommands");
|
|
9
9
|
const { runGroupCoreCommand } = require("./cli/groupCoreCommands");
|
|
10
|
+
const { loadConfig } = require("./config");
|
|
11
|
+
const { loadPromptProfileRegistry } = require("./group/promptProfiles");
|
|
12
|
+
const { resolveSoloAgentType } = require("./solo/commands");
|
|
10
13
|
const { listProjectRuntimes, getCurrentProjectRuntime } = require("./projects/registry");
|
|
11
14
|
const { canonicalProjectRoot, buildProjectId } = require("./projects/projectId");
|
|
12
15
|
const { getUfooPaths } = require("./ufoo/paths");
|
|
@@ -448,6 +451,68 @@ async function runCli(argv) {
|
|
|
448
451
|
process.exitCode = 1;
|
|
449
452
|
}
|
|
450
453
|
});
|
|
454
|
+
program
|
|
455
|
+
.command("solo")
|
|
456
|
+
.description("Solo role agent operations")
|
|
457
|
+
.argument("<action>", "run|list")
|
|
458
|
+
.argument("[profile]", "Prompt profile id or alias")
|
|
459
|
+
.option("--agent <type>", "Agent type: codex|claude|ucode")
|
|
460
|
+
.option("--nickname <name>", "Optional nickname")
|
|
461
|
+
.option("--scope <scope>", "Launch scope: inplace|window", "inplace")
|
|
462
|
+
.option("--json", "Output role list as JSON")
|
|
463
|
+
.action(async (action, profile, opts) => {
|
|
464
|
+
try {
|
|
465
|
+
const projectRoot = process.cwd();
|
|
466
|
+
const subcommand = String(action || "").trim().toLowerCase();
|
|
467
|
+
if (subcommand === "list" || subcommand === "ls") {
|
|
468
|
+
const registry = loadPromptProfileRegistry(projectRoot);
|
|
469
|
+
if (opts.json) {
|
|
470
|
+
console.log(JSON.stringify({ profiles: registry.profiles || [] }, null, 2));
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const profiles = registry.profiles || [];
|
|
474
|
+
if (profiles.length === 0) {
|
|
475
|
+
console.log("No solo roles found.");
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
console.log(`Available solo roles (${profiles.length}):`);
|
|
479
|
+
for (const p of profiles) {
|
|
480
|
+
const aliases = p.aliases && p.aliases.length > 0 ? ` (${p.aliases.join(", ")})` : "";
|
|
481
|
+
const source = p.source ? ` [${p.source}]` : "";
|
|
482
|
+
console.log(` ${p.id}${aliases}${source}`);
|
|
483
|
+
if (p.summary) console.log(` ${p.summary}`);
|
|
484
|
+
}
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
if (subcommand !== "run") {
|
|
488
|
+
throw new Error(`Unknown solo action: ${subcommand}`);
|
|
489
|
+
}
|
|
490
|
+
const promptProfile = String(profile || "").trim();
|
|
491
|
+
if (!promptProfile) {
|
|
492
|
+
throw new Error("solo run requires <profile>");
|
|
493
|
+
}
|
|
494
|
+
await ensureDaemonRunning(projectRoot);
|
|
495
|
+
const config = loadConfig(projectRoot);
|
|
496
|
+
const agent = resolveSoloAgentType(config, opts.agent || "");
|
|
497
|
+
const scope = String(opts.scope || "inplace").trim().toLowerCase();
|
|
498
|
+
if (scope !== "inplace" && scope !== "window") {
|
|
499
|
+
throw new Error("scope must be inplace|window");
|
|
500
|
+
}
|
|
501
|
+
const resp = await sendDaemonRequest(projectRoot, {
|
|
502
|
+
type: "launch_agent",
|
|
503
|
+
agent: agent === "ucode" ? "ufoo" : agent,
|
|
504
|
+
nickname: String(opts.nickname || "").trim(),
|
|
505
|
+
prompt_profile: promptProfile,
|
|
506
|
+
count: 1,
|
|
507
|
+
launch_scope: scope,
|
|
508
|
+
...collectHostLaunchRequestContext(),
|
|
509
|
+
});
|
|
510
|
+
console.log(resp?.data?.reply || `Launched ${agent} role ${promptProfile}`);
|
|
511
|
+
} catch (err) {
|
|
512
|
+
console.error(err.message || String(err));
|
|
513
|
+
process.exitCode = 1;
|
|
514
|
+
}
|
|
515
|
+
});
|
|
451
516
|
program
|
|
452
517
|
.command("role")
|
|
453
518
|
.description("Assign a preset role to an existing agent")
|
|
@@ -1262,6 +1327,38 @@ async function runCli(argv) {
|
|
|
1262
1327
|
}
|
|
1263
1328
|
});
|
|
1264
1329
|
|
|
1330
|
+
const history = program.command("history").description("Agent input history timeline");
|
|
1331
|
+
history
|
|
1332
|
+
.command("build")
|
|
1333
|
+
.description("Build unified input timeline (incremental by default)")
|
|
1334
|
+
.option("--force", "Full rebuild (ignore watermark)")
|
|
1335
|
+
.action((opts) => {
|
|
1336
|
+
const { buildTimeline } = require("./history/inputTimeline");
|
|
1337
|
+
const result = buildTimeline(process.cwd(), { force: opts.force === true });
|
|
1338
|
+
console.log(`Timeline: ${result.count} total, ${result.newCount} new → ${result.file}`);
|
|
1339
|
+
});
|
|
1340
|
+
history
|
|
1341
|
+
.command("show")
|
|
1342
|
+
.description("Show recent timeline entries")
|
|
1343
|
+
.argument("[limit]", "Number of entries to show", "50")
|
|
1344
|
+
.action((limit) => {
|
|
1345
|
+
const { showTimeline } = require("./history/inputTimeline");
|
|
1346
|
+
showTimeline(process.cwd(), parseInt(limit, 10) || 50);
|
|
1347
|
+
});
|
|
1348
|
+
history
|
|
1349
|
+
.command("prompt")
|
|
1350
|
+
.description("Render timeline as injectable prompt context")
|
|
1351
|
+
.argument("[limit]", "Number of entries to include", "30")
|
|
1352
|
+
.action((limit) => {
|
|
1353
|
+
const { renderTimelineForPrompt } = require("./history/inputTimeline");
|
|
1354
|
+
const text = renderTimelineForPrompt(process.cwd(), parseInt(limit, 10) || 30);
|
|
1355
|
+
if (text) {
|
|
1356
|
+
console.log(text);
|
|
1357
|
+
} else {
|
|
1358
|
+
console.log("No timeline data. Run `ufoo history build` first.");
|
|
1359
|
+
}
|
|
1360
|
+
});
|
|
1361
|
+
|
|
1265
1362
|
program.addHelpText(
|
|
1266
1363
|
"after",
|
|
1267
1364
|
`\nNotes:\n - If 'ufoo' isn't in PATH, run it via ${chalk.cyan(
|
|
@@ -1312,6 +1409,8 @@ async function runCli(argv) {
|
|
|
1312
1409
|
console.log(" ufoo group status [groupId] [--json]");
|
|
1313
1410
|
console.log(" ufoo group diagram <alias|groupId> [--ascii|--mermaid] [--json]");
|
|
1314
1411
|
console.log(" ufoo group stop <groupId> [--json]");
|
|
1412
|
+
console.log(" ufoo solo list [--json]");
|
|
1413
|
+
console.log(" ufoo solo run <profile> [--agent <codex|claude|ucode>] [--nickname <name>] [--scope <inplace|window>]");
|
|
1315
1414
|
console.log(" ufoo online server [--port 8787] [--host 127.0.0.1] [--token-file <path>]");
|
|
1316
1415
|
console.log(" ufoo online token <subscriber> [--nickname <name>] [--server <url>] [--file <path>]");
|
|
1317
1416
|
console.log(" ufoo online room create [--name <room>] --type public|private [--password <pwd>] [--created-by <name>] [--server <url>]");
|
|
@@ -1324,6 +1423,7 @@ async function runCli(argv) {
|
|
|
1324
1423
|
console.log(" ufoo bus wake <target> [--reason <reason>] [--no-shake]");
|
|
1325
1424
|
console.log(" ufoo bus <args...> (JS bus implementation)");
|
|
1326
1425
|
console.log(" ufoo ctx <subcmd> ... (doctor|lint|decisions|sync)");
|
|
1426
|
+
console.log(" ufoo history <build|show|prompt> [limit]");
|
|
1327
1427
|
console.log("");
|
|
1328
1428
|
console.log("Notes:");
|
|
1329
1429
|
console.log(" - For Codex notifications, use ufoo bus alert / ufoo bus listen");
|
|
@@ -1795,6 +1895,67 @@ async function runCli(argv) {
|
|
|
1795
1895
|
})();
|
|
1796
1896
|
return;
|
|
1797
1897
|
}
|
|
1898
|
+
if (cmd === "solo") {
|
|
1899
|
+
const sub = String(rest[0] || "").trim().toLowerCase();
|
|
1900
|
+
(async () => {
|
|
1901
|
+
try {
|
|
1902
|
+
const projectRoot = process.cwd();
|
|
1903
|
+
if (sub === "list" || sub === "ls") {
|
|
1904
|
+
const outputJson = rest.includes("--json");
|
|
1905
|
+
const registry = loadPromptProfileRegistry(projectRoot);
|
|
1906
|
+
if (outputJson) {
|
|
1907
|
+
console.log(JSON.stringify({ profiles: registry.profiles || [] }, null, 2));
|
|
1908
|
+
return;
|
|
1909
|
+
}
|
|
1910
|
+
const profiles = registry.profiles || [];
|
|
1911
|
+
if (profiles.length === 0) {
|
|
1912
|
+
console.log("No solo roles found.");
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
console.log(`Available solo roles (${profiles.length}):`);
|
|
1916
|
+
for (const p of profiles) {
|
|
1917
|
+
const aliases = p.aliases && p.aliases.length > 0 ? ` (${p.aliases.join(", ")})` : "";
|
|
1918
|
+
const source = p.source ? ` [${p.source}]` : "";
|
|
1919
|
+
console.log(` ${p.id}${aliases}${source}`);
|
|
1920
|
+
if (p.summary) console.log(` ${p.summary}`);
|
|
1921
|
+
}
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
if (sub === "run") {
|
|
1925
|
+
const profile = String(rest[1] || "").trim();
|
|
1926
|
+
if (!profile) throw new Error("solo run requires <profile>");
|
|
1927
|
+
const agentIdx = rest.indexOf("--agent");
|
|
1928
|
+
const agentInput = agentIdx !== -1 ? String(rest[agentIdx + 1] || "").trim() : "";
|
|
1929
|
+
const nickIdx = rest.indexOf("--nickname");
|
|
1930
|
+
const nickname = nickIdx !== -1 ? String(rest[nickIdx + 1] || "").trim() : "";
|
|
1931
|
+
const scopeIdx = rest.indexOf("--scope");
|
|
1932
|
+
const scope = scopeIdx !== -1 ? String(rest[scopeIdx + 1] || "").trim().toLowerCase() : "inplace";
|
|
1933
|
+
if (scope !== "inplace" && scope !== "window") {
|
|
1934
|
+
throw new Error("scope must be inplace|window");
|
|
1935
|
+
}
|
|
1936
|
+
await ensureDaemonRunning(projectRoot);
|
|
1937
|
+
const config = loadConfig(projectRoot);
|
|
1938
|
+
const agent = resolveSoloAgentType(config, agentInput);
|
|
1939
|
+
const resp = await sendDaemonRequest(projectRoot, {
|
|
1940
|
+
type: "launch_agent",
|
|
1941
|
+
agent: agent === "ucode" ? "ufoo" : agent,
|
|
1942
|
+
nickname,
|
|
1943
|
+
prompt_profile: profile,
|
|
1944
|
+
count: 1,
|
|
1945
|
+
launch_scope: scope,
|
|
1946
|
+
...collectHostLaunchRequestContext(),
|
|
1947
|
+
});
|
|
1948
|
+
console.log(resp?.data?.reply || `Launched ${agent} role ${profile}`);
|
|
1949
|
+
return;
|
|
1950
|
+
}
|
|
1951
|
+
throw new Error(`Unknown solo action: ${sub || "(empty)"}`);
|
|
1952
|
+
} catch (err) {
|
|
1953
|
+
console.error(err.message || String(err));
|
|
1954
|
+
process.exitCode = 1;
|
|
1955
|
+
}
|
|
1956
|
+
})();
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1798
1959
|
if (cmd === "online") {
|
|
1799
1960
|
const sub = rest[0] || "";
|
|
1800
1961
|
if (!sub) {
|
|
@@ -1971,6 +2132,36 @@ async function runCli(argv) {
|
|
|
1971
2132
|
return;
|
|
1972
2133
|
}
|
|
1973
2134
|
|
|
2135
|
+
if (cmd === "history") {
|
|
2136
|
+
const sub = rest[0] || "show";
|
|
2137
|
+
const cwd = process.cwd();
|
|
2138
|
+
const { buildTimeline, showTimeline, renderTimelineForPrompt } = require("./history/inputTimeline");
|
|
2139
|
+
|
|
2140
|
+
if (sub === "build") {
|
|
2141
|
+
const force = rest.includes("--force");
|
|
2142
|
+
const result = buildTimeline(cwd, { force });
|
|
2143
|
+
console.log(`Timeline: ${result.count} total, ${result.newCount} new → ${result.file}`);
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
2146
|
+
if (sub === "show") {
|
|
2147
|
+
const limit = parseInt(rest[1], 10) || 50;
|
|
2148
|
+
showTimeline(cwd, limit);
|
|
2149
|
+
return;
|
|
2150
|
+
}
|
|
2151
|
+
if (sub === "prompt") {
|
|
2152
|
+
const limit = parseInt(rest[1], 10) || 30;
|
|
2153
|
+
const text = renderTimelineForPrompt(cwd, limit);
|
|
2154
|
+
if (text) {
|
|
2155
|
+
console.log(text);
|
|
2156
|
+
} else {
|
|
2157
|
+
console.log("No timeline data. Run `ufoo history build` first.");
|
|
2158
|
+
}
|
|
2159
|
+
return;
|
|
2160
|
+
}
|
|
2161
|
+
console.log("Usage: ufoo history [build|show|prompt] [limit]");
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
2164
|
+
|
|
1974
2165
|
help();
|
|
1975
2166
|
process.exitCode = 1;
|
|
1976
2167
|
}
|
|
@@ -753,6 +753,7 @@ function createGroupOrchestrator(options = {}) {
|
|
|
753
753
|
}
|
|
754
754
|
|
|
755
755
|
const plan = buildLaunchPlan(validated.entry.data);
|
|
756
|
+
const templateDefaults = (validated.entry.data && validated.entry.data.defaults) || {};
|
|
756
757
|
const groupId = generateGroupId(validated.entry.alias || alias, instance);
|
|
757
758
|
const projectNicknamePrefix = buildProjectNicknamePrefix(projectRoot);
|
|
758
759
|
|
|
@@ -835,6 +836,12 @@ function createGroupOrchestrator(options = {}) {
|
|
|
835
836
|
const item = compiled.executionPlan[i];
|
|
836
837
|
const member = runtime.members[i];
|
|
837
838
|
const extraEnv = {};
|
|
839
|
+
if (item.bootstrap_required) {
|
|
840
|
+
extraEnv.UFOO_SKIP_DEFAULT_BOOTSTRAP = "1";
|
|
841
|
+
}
|
|
842
|
+
if (Number.isInteger(templateDefaults.start_timeout_ms) && templateDefaults.start_timeout_ms > 0) {
|
|
843
|
+
extraEnv.UFOO_REGISTER_TIMEOUT_MS = String(templateDefaults.start_timeout_ms);
|
|
844
|
+
}
|
|
838
845
|
let extraArgs = [];
|
|
839
846
|
let bootstrapInjected = false;
|
|
840
847
|
|
package/src/daemon/index.js
CHANGED
|
@@ -12,7 +12,7 @@ const { generateInstanceId, subscriberToSafeName } = require("../bus/utils");
|
|
|
12
12
|
const { createDaemonIpcServer } = require("./ipcServer");
|
|
13
13
|
const { IPC_REQUEST_TYPES, IPC_RESPONSE_TYPES, BUS_STATUS_PHASES } = require("../shared/eventContract");
|
|
14
14
|
const { getUfooPaths } = require("../ufoo/paths");
|
|
15
|
-
const { upsertProjectRuntime, markProjectStopped } = require("../projects
|
|
15
|
+
const { upsertProjectRuntime, markProjectStopped } = require("../projects");
|
|
16
16
|
const { scheduleProviderSessionProbe, resolveSessionFromFile, persistProviderSession, loadProviderSessionCache } = require("./providerSessions");
|
|
17
17
|
const { createTerminalAdapterRouter } = require("../terminal/adapterRouter");
|
|
18
18
|
const { createDaemonCronController } = require("./cronOps");
|
|
@@ -22,7 +22,7 @@ const { runAssistantTask } = require("../assistant/bridge");
|
|
|
22
22
|
const { runPromptWithAssistant } = require("./promptLoop");
|
|
23
23
|
const { handlePromptRequest } = require("./promptRequest");
|
|
24
24
|
const { recordAgentReport } = require("./reporting");
|
|
25
|
-
const { isGlobalControllerProjectRoot } = require("../
|
|
25
|
+
const { isGlobalControllerProjectRoot } = require("../projects");
|
|
26
26
|
const {
|
|
27
27
|
assignSoloRoleToExistingAgent,
|
|
28
28
|
resolveSoloPromptProfile,
|
|
@@ -201,16 +201,16 @@ function looksLikeRunningDaemon(projectRoot, pid) {
|
|
|
201
201
|
function isRunning(projectRoot) {
|
|
202
202
|
const pid = readPid(projectRoot);
|
|
203
203
|
if (!pid) return false;
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
204
|
+
return looksLikeRunningDaemon(projectRoot, pid);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function cleanupStaleState(projectRoot) {
|
|
207
208
|
try {
|
|
208
209
|
fs.unlinkSync(pidPath(projectRoot));
|
|
209
210
|
} catch {
|
|
210
211
|
// ignore
|
|
211
212
|
}
|
|
212
213
|
removeSocket(projectRoot);
|
|
213
|
-
return false;
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
function removeSocket(projectRoot) {
|
|
@@ -1051,6 +1051,7 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1051
1051
|
await init.init({ modules: "context,bus", project: root });
|
|
1052
1052
|
}
|
|
1053
1053
|
if (!isRunning(root)) {
|
|
1054
|
+
cleanupStaleState(root);
|
|
1054
1055
|
const daemonBin = path.join(__dirname, "..", "..", "bin", "ufoo.js");
|
|
1055
1056
|
const child = spawn(process.execPath, [daemonBin, "daemon", "--start"], {
|
|
1056
1057
|
detached: true,
|
|
@@ -1384,6 +1385,12 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
1384
1385
|
}
|
|
1385
1386
|
}
|
|
1386
1387
|
}
|
|
1388
|
+
if (requestedProfile) {
|
|
1389
|
+
op.extra_env = {
|
|
1390
|
+
...(op.extra_env && typeof op.extra_env === "object" ? op.extra_env : {}),
|
|
1391
|
+
UFOO_SKIP_DEFAULT_BOOTSTRAP: "1",
|
|
1392
|
+
};
|
|
1393
|
+
}
|
|
1387
1394
|
try {
|
|
1388
1395
|
const opsResults = await handleOps(projectRoot, [op], processManager);
|
|
1389
1396
|
const launchResult = opsResults.find((r) => r.action === "launch");
|
|
@@ -2064,43 +2071,61 @@ function startDaemon({ projectRoot, provider, model, resumeMode = "auto" }) {
|
|
|
2064
2071
|
}
|
|
2065
2072
|
log(`agent_ready id=${subscriberId} pid=${agentPid || 0} - resolving session`);
|
|
2066
2073
|
|
|
2067
|
-
// Try direct file read first if we have agentPid (fast path)
|
|
2068
2074
|
const parsedAgentPid = Number.parseInt(agentPid, 10);
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
});
|
|
2075
|
+
const agentType = subscriberId.split(":")[0] || "";
|
|
2076
|
+
|
|
2077
|
+
// In _spawnDirect (host mode), AGENT_READY is sent immediately after
|
|
2078
|
+
// spawn() before Claude has written ~/.claude/sessions/<pid>.json.
|
|
2079
|
+
// Retry with short backoff to handle the race condition.
|
|
2080
|
+
const RETRY_DELAYS_MS = [100, 500, 1000, 2000, 3000];
|
|
2081
|
+
|
|
2082
|
+
const tryResolveSession = (attempt) => {
|
|
2083
|
+
if (Number.isFinite(parsedAgentPid) && parsedAgentPid > 0) {
|
|
2084
|
+
const resolved = resolveSessionFromFile(agentType, {
|
|
2085
|
+
pid: parsedAgentPid,
|
|
2086
|
+
cwd: projectRoot,
|
|
2087
|
+
});
|
|
2088
|
+
if (resolved && resolved.sessionId) {
|
|
2089
|
+
const attemptNote = attempt > 1 ? ` (attempt ${attempt})` : "";
|
|
2090
|
+
log(`agent_ready session resolved from file for ${subscriberId}: ${resolved.sessionId}${attemptNote}`);
|
|
2091
|
+
persistProviderSession(projectRoot, subscriberId, resolved);
|
|
2092
|
+
if (providerSessions) {
|
|
2093
|
+
providerSessions.set(subscriberId, {
|
|
2094
|
+
sessionId: resolved.sessionId,
|
|
2095
|
+
source: resolved.source || "",
|
|
2096
|
+
updated_at: new Date().toISOString(),
|
|
2097
|
+
});
|
|
2098
|
+
}
|
|
2099
|
+
// Cancel the scheduled probe to prevent redundant /ufoo injection
|
|
2100
|
+
const handle = probeHandles.get(subscriberId);
|
|
2101
|
+
if (handle && typeof handle.cancel === "function") {
|
|
2102
|
+
handle.cancel();
|
|
2103
|
+
}
|
|
2104
|
+
probeHandles.delete(subscriberId);
|
|
2105
|
+
return;
|
|
2084
2106
|
}
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2107
|
+
|
|
2108
|
+
// Session file not ready — retry if attempts remain
|
|
2109
|
+
const delayMs = RETRY_DELAYS_MS[attempt - 1];
|
|
2110
|
+
if (delayMs !== undefined) {
|
|
2111
|
+
setTimeout(() => tryResolveSession(attempt + 1), delayMs);
|
|
2112
|
+
return;
|
|
2089
2113
|
}
|
|
2090
|
-
probeHandles.delete(subscriberId);
|
|
2091
|
-
return;
|
|
2092
2114
|
}
|
|
2093
|
-
}
|
|
2094
2115
|
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2116
|
+
// Exhausted retries or no pid — fall back to scheduled probe
|
|
2117
|
+
const probeHandle = probeHandles.get(subscriberId);
|
|
2118
|
+
if (probeHandle && typeof probeHandle.triggerNow === "function") {
|
|
2119
|
+
log(`agent_ready falling back to probe for ${subscriberId}`);
|
|
2120
|
+
probeHandle.triggerNow().catch((err) => {
|
|
2121
|
+
log(`agent_ready probe trigger failed for ${subscriberId}: ${err.message}`);
|
|
2122
|
+
});
|
|
2123
|
+
} else {
|
|
2124
|
+
log(`agent_ready no probe handle found for ${subscriberId}`);
|
|
2125
|
+
}
|
|
2126
|
+
};
|
|
2127
|
+
|
|
2128
|
+
tryResolveSession(1);
|
|
2104
2129
|
return;
|
|
2105
2130
|
}
|
|
2106
2131
|
};
|
|
@@ -2323,4 +2348,4 @@ function stopDaemon(projectRoot) {
|
|
|
2323
2348
|
return killed;
|
|
2324
2349
|
}
|
|
2325
2350
|
|
|
2326
|
-
module.exports = { startDaemon, stopDaemon, isRunning, socketPath };
|
|
2351
|
+
module.exports = { startDaemon, stopDaemon, isRunning, cleanupStaleState, socketPath };
|
|
@@ -5,7 +5,7 @@ const {
|
|
|
5
5
|
listControllerInboxEntries,
|
|
6
6
|
consumeControllerInboxEntries,
|
|
7
7
|
} = require("../report/store");
|
|
8
|
-
const { isGlobalControllerProjectRoot } = require("../
|
|
8
|
+
const { isGlobalControllerProjectRoot } = require("../projects");
|
|
9
9
|
|
|
10
10
|
function normalizeProjectRoute(route) {
|
|
11
11
|
if (!route || typeof route !== "object") return null;
|
package/src/group/bootstrap.js
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
const crypto = require("crypto");
|
|
4
4
|
|
|
5
|
+
const SHARED_UFOO_PROTOCOL = [
|
|
6
|
+
"ufoo protocol:",
|
|
7
|
+
"- At session start, sync shared context with `ufoo ctx decisions -l` and `ufoo ctx decisions -n 1`.",
|
|
8
|
+
"- Record a decision ONLY for important, plan-level knowledge: architectural choices, multi-option trade-off analysis, cross-agent coordination decisions, or plans that affect other agents. Do NOT record routine findings, simple bug fixes, or trivial observations. Use `ufoo ctx decisions new \"Title\"` BEFORE acting.",
|
|
9
|
+
"- Use `ufoo bus send <target-nickname> \"<message>\"` for agent-to-agent handoffs.",
|
|
10
|
+
"- If you receive pending bus work, execute it immediately, reply to the sender, then `ufoo bus ack \"$UFOO_SUBSCRIBER_ID\"`.",
|
|
11
|
+
"- Use `ufoo report` for controller/runtime status updates, not as a substitute for direct handoffs.",
|
|
12
|
+
].join("\n");
|
|
13
|
+
|
|
5
14
|
const SHARED_GROUP_PREFIX = [
|
|
6
15
|
"You are part of a ufoo multi-agent group.",
|
|
7
16
|
"",
|
|
@@ -13,6 +22,8 @@ const SHARED_GROUP_PREFIX = [
|
|
|
13
22
|
"- When reporting, separate facts, inferences, and recommendations.",
|
|
14
23
|
"- Preserve continuity with the group's current task rather than restarting analysis from scratch.",
|
|
15
24
|
"",
|
|
25
|
+
SHARED_UFOO_PROTOCOL,
|
|
26
|
+
"",
|
|
16
27
|
"Coordination protocol:",
|
|
17
28
|
"- Use direct handoff for worker-to-worker delivery.",
|
|
18
29
|
"- Use private `ufoo report` updates for ufoo-agent control-plane reporting.",
|
|
@@ -28,6 +39,8 @@ const SOLO_AGENT_PREFIX = [
|
|
|
28
39
|
"- Surface uncertainty explicitly.",
|
|
29
40
|
"- Preserve continuity with the current task instead of restarting from scratch.",
|
|
30
41
|
"- Use ufoo-agent for control-plane coordination, not as a substitute for doing your role.",
|
|
42
|
+
"",
|
|
43
|
+
SHARED_UFOO_PROTOCOL,
|
|
31
44
|
].join("\n");
|
|
32
45
|
|
|
33
46
|
function asTrimmedString(value) {
|
|
@@ -145,6 +158,7 @@ function computeBootstrapFingerprint({
|
|
|
145
158
|
}
|
|
146
159
|
|
|
147
160
|
module.exports = {
|
|
161
|
+
SHARED_UFOO_PROTOCOL,
|
|
148
162
|
SHARED_GROUP_PREFIX,
|
|
149
163
|
SOLO_AGENT_PREFIX,
|
|
150
164
|
buildGroupPromptMetadata,
|