u-foo 1.7.4 → 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 +185 -6
- package/src/assistant/constants.js +1 -1
- package/src/assistant/engine.js +1 -6
- package/src/chat/commandExecutor.js +116 -19
- package/src/chat/commands.js +8 -1
- package/src/chat/completionController.js +40 -0
- package/src/chat/cronScheduler.js +37 -6
- package/src/chat/daemonMessageRouter.js +23 -3
- package/src/chat/dashboardKeyController.js +48 -59
- package/src/chat/dashboardView.js +31 -39
- package/src/chat/index.js +154 -77
- package/src/chat/inputListenerController.js +14 -0
- package/src/chat/inputSubmitHandler.js +9 -5
- package/src/chat/settingsController.js +0 -28
- package/src/chat/transientAgentState.js +64 -0
- package/src/cli/groupCoreCommands.js +21 -12
- package/src/cli.js +23 -1
- package/src/daemon/cronOps.js +48 -11
- package/src/daemon/groupOrchestrator.js +581 -97
- package/src/daemon/index.js +420 -5
- 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 +7 -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
package/README.md
CHANGED
|
@@ -113,13 +113,16 @@ ucode-core list --json
|
|
|
113
113
|
|
|
114
114
|
## Global Chat (`ufoo -g`)
|
|
115
115
|
|
|
116
|
-
Use `ufoo -g` (or `ufoo --global`) to launch a cross-project chat dashboard. Instead of being scoped to
|
|
116
|
+
Use `ufoo -g` (or `ufoo --global`) to launch a cross-project chat dashboard. Instead of being scoped to the current working directory, global mode runs from a home-scoped controller context and stores its runtime under `~/.ufoo`, then connects to project daemons on demand.
|
|
117
|
+
|
|
118
|
+
When global chat stays on the home-scoped controller, plain prompts first go through the controller's `ufoo-agent`, which picks the best registered project and forwards the prompt to that project's `ufoo-agent` for agent-level routing.
|
|
117
119
|
|
|
118
120
|
```bash
|
|
119
121
|
$ ufoo -g
|
|
120
122
|
|
|
121
123
|
> /project list # List all running project daemons
|
|
122
124
|
> /project switch 2 # Switch to project #2
|
|
125
|
+
> /open ~/Code/my-app # Initialize/start/switch to a project by path
|
|
123
126
|
> /launch claude scope=inplace # Launch agent in current context
|
|
124
127
|
> @claude-1 Start reviewing the auth module
|
|
125
128
|
```
|
|
@@ -128,9 +131,14 @@ $ ufoo -g
|
|
|
128
131
|
|---------|-------------|
|
|
129
132
|
| `/project list` | List running projects from global runtime registry |
|
|
130
133
|
| `/project switch <index\|path>` | Switch active project daemon connection |
|
|
134
|
+
| `/open <path>` | Global-mode shortcut to initialize/start/open a project daemon by path |
|
|
131
135
|
| `/launch <agent> scope=inplace` | Launch agent in current workspace |
|
|
132
136
|
| `/launch <agent> scope=window` | Launch agent in separate terminal window |
|
|
133
137
|
|
|
138
|
+
Notes:
|
|
139
|
+
- If you just type a normal message in the controller view, global `ufoo-agent` will try to route it to the most relevant registered project first.
|
|
140
|
+
- The selected project's `ufoo-agent` then continues the second-hop routing to a concrete coding agent.
|
|
141
|
+
|
|
134
142
|
## Agent Configuration
|
|
135
143
|
|
|
136
144
|
Configure AI providers in `.ufoo/config.json`:
|
package/README.zh-CN.md
CHANGED
|
@@ -113,13 +113,16 @@ ucode-core list --json
|
|
|
113
113
|
|
|
114
114
|
## 全局聊天(`ufoo -g`)
|
|
115
115
|
|
|
116
|
-
使用 `ufoo -g`(或 `ufoo --global
|
|
116
|
+
使用 `ufoo -g`(或 `ufoo --global`)启动跨项目聊天仪表盘。全局模式不再绑定当前工作目录,而是使用一个基于家目录的控制器上下文,并将自身运行时写入 `~/.ufoo`,然后按需连接各项目的 ufoo 守护进程。
|
|
117
|
+
|
|
118
|
+
当全局聊天停留在这个家目录控制器视图时,普通消息会先经过控制器侧的 `ufoo-agent`,由它在当前已注册的 Projects 里选择最合适的项目,再把消息转交给目标项目内的 `ufoo-agent` 继续做 agent 级路由。
|
|
117
119
|
|
|
118
120
|
```bash
|
|
119
121
|
$ ufoo -g
|
|
120
122
|
|
|
121
123
|
> /project list # 列出所有运行中的项目守护进程
|
|
122
124
|
> /project switch 2 # 切换到第 2 个项目
|
|
125
|
+
> /open ~/Code/my-app # 按路径初始化/启动/打开一个项目
|
|
123
126
|
> /launch claude scope=inplace # 在当前上下文启动 Agent
|
|
124
127
|
> @claude-1 开始审查 auth 模块
|
|
125
128
|
```
|
|
@@ -128,9 +131,14 @@ $ ufoo -g
|
|
|
128
131
|
|------|------|
|
|
129
132
|
| `/project list` | 列出全局运行时注册的项目 |
|
|
130
133
|
| `/project switch <序号\|路径>` | 切换活动项目的 daemon 连接 |
|
|
134
|
+
| `/open <path>` | 仅在全局模式下可用;按路径初始化、启动并打开项目 daemon |
|
|
131
135
|
| `/launch <agent> scope=inplace` | 在当前工作区启动 Agent |
|
|
132
136
|
| `/launch <agent> scope=window` | 在独立终端窗口启动 Agent |
|
|
133
137
|
|
|
138
|
+
说明:
|
|
139
|
+
- 如果你在控制器视图里直接输入普通消息,全局 `ufoo-agent` 会先尝试把它路由到最相关的已注册项目。
|
|
140
|
+
- 选中的项目里,项目侧 `ufoo-agent` 会继续完成第二跳路由,选择具体 coding agent。
|
|
141
|
+
|
|
134
142
|
## Agent 配置
|
|
135
143
|
|
|
136
144
|
在 `.ufoo/config.json` 中配置 AI 提供商:
|
package/bin/ufoo.js
CHANGED
|
@@ -5,6 +5,7 @@ const { runDaemonCli } = require("../src/daemon/run");
|
|
|
5
5
|
const { runChat } = require("../src/chat");
|
|
6
6
|
const { runInternalRunner } = require("../src/agent/internalRunner");
|
|
7
7
|
const { runPtyRunner } = require("../src/agent/ptyRunner");
|
|
8
|
+
const { resolveGlobalControllerProjectRoot } = require("../src/globalMode");
|
|
8
9
|
|
|
9
10
|
const rawArgv = process.argv.slice(2);
|
|
10
11
|
|
|
@@ -16,9 +17,10 @@ async function main() {
|
|
|
16
17
|
const globalMode = hasGlobalModeFlag(rawArgv);
|
|
17
18
|
const argv = rawArgv.filter((arg) => arg !== "-g" && arg !== "--global");
|
|
18
19
|
const cmd = argv[0];
|
|
20
|
+
const chatProjectRoot = globalMode ? resolveGlobalControllerProjectRoot() : process.cwd();
|
|
19
21
|
|
|
20
22
|
if (!cmd) {
|
|
21
|
-
await runChat(
|
|
23
|
+
await runChat(chatProjectRoot, { globalMode });
|
|
22
24
|
return;
|
|
23
25
|
}
|
|
24
26
|
if (cmd === "daemon") {
|
|
@@ -47,7 +49,7 @@ async function main() {
|
|
|
47
49
|
return;
|
|
48
50
|
}
|
|
49
51
|
if (cmd === "chat") {
|
|
50
|
-
await runChat(
|
|
52
|
+
await runChat(chatProjectRoot, { globalMode });
|
|
51
53
|
return;
|
|
52
54
|
}
|
|
53
55
|
|
package/package.json
CHANGED
package/src/agent/cliRunner.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { spawn } = require("child_process");
|
|
2
2
|
const { randomUUID } = require("crypto");
|
|
3
|
+
const { DEFAULT_ASSISTANT_TIMEOUT_MS } = require("../assistant/constants");
|
|
3
4
|
|
|
4
5
|
const ROUTER_JSON_SCHEMA = JSON.stringify({
|
|
5
6
|
type: "object",
|
|
@@ -616,7 +617,7 @@ async function runCliAgent(params) {
|
|
|
616
617
|
cwd: params.cwd,
|
|
617
618
|
env,
|
|
618
619
|
input: stdin,
|
|
619
|
-
timeoutMs: params.timeoutMs ||
|
|
620
|
+
timeoutMs: params.timeoutMs || DEFAULT_ASSISTANT_TIMEOUT_MS,
|
|
620
621
|
onStdout: codexParser ? (chunk) => codexParser.onChunk(chunk) : null,
|
|
621
622
|
signal: params.signal,
|
|
622
623
|
});
|
|
@@ -678,7 +679,7 @@ async function runCliAgent(params) {
|
|
|
678
679
|
cwd: params.cwd,
|
|
679
680
|
env,
|
|
680
681
|
input: retryStdin,
|
|
681
|
-
timeoutMs: params.timeoutMs ||
|
|
682
|
+
timeoutMs: params.timeoutMs || DEFAULT_ASSISTANT_TIMEOUT_MS,
|
|
682
683
|
onStdout: retryParser ? (chunk) => retryParser.onChunk(chunk) : null,
|
|
683
684
|
signal: params.signal,
|
|
684
685
|
});
|
|
@@ -77,18 +77,20 @@ function buildBootstrapContent({
|
|
|
77
77
|
function prepareUcodeBootstrap({
|
|
78
78
|
projectRoot = process.cwd(),
|
|
79
79
|
promptFile = "",
|
|
80
|
+
promptText = "",
|
|
80
81
|
targetFile = "",
|
|
81
82
|
} = {}) {
|
|
82
83
|
const resolvedProjectRoot = path.resolve(projectRoot);
|
|
83
84
|
const resolvedPrompt = String(promptFile || "").trim();
|
|
84
85
|
const resolvedTarget = String(targetFile || "").trim() || defaultBootstrapPath(resolvedProjectRoot);
|
|
85
86
|
|
|
86
|
-
const
|
|
87
|
+
const inlinePromptText = String(promptText || "").trim();
|
|
88
|
+
const resolvedPromptText = inlinePromptText || readFileSafe(resolvedPrompt);
|
|
87
89
|
const rules = resolveProjectRules(resolvedProjectRoot);
|
|
88
90
|
const content = buildBootstrapContent({
|
|
89
91
|
projectRoot: resolvedProjectRoot,
|
|
90
92
|
promptFile: resolvedPrompt,
|
|
91
|
-
promptText,
|
|
93
|
+
promptText: resolvedPromptText,
|
|
92
94
|
rules,
|
|
93
95
|
});
|
|
94
96
|
|
|
@@ -99,7 +101,7 @@ function prepareUcodeBootstrap({
|
|
|
99
101
|
ok: true,
|
|
100
102
|
file: resolvedTarget,
|
|
101
103
|
promptFile: resolvedPrompt,
|
|
102
|
-
hasPrompt: Boolean(
|
|
104
|
+
hasPrompt: Boolean(resolvedPromptText.trim()),
|
|
103
105
|
rulesCount: rules.length,
|
|
104
106
|
};
|
|
105
107
|
}
|
package/src/agent/ufooAgent.js
CHANGED
|
@@ -11,6 +11,8 @@ const {
|
|
|
11
11
|
} = require("../code/nativeRunner");
|
|
12
12
|
const { DEFAULT_ASSISTANT_TIMEOUT_MS } = require("../assistant/constants");
|
|
13
13
|
const { normalizeAgentTypeAlias } = require("../bus/utils");
|
|
14
|
+
const { listProjectRuntimes } = require("../projects/registry");
|
|
15
|
+
const { isGlobalControllerProjectRoot } = require("../globalMode");
|
|
14
16
|
|
|
15
17
|
function loadSessionState(projectRoot) {
|
|
16
18
|
const dir = getUfooPaths(projectRoot).agentDir;
|
|
@@ -260,7 +262,180 @@ function loadBusSummary(projectRoot, maxLines = 20) {
|
|
|
260
262
|
return { agents, nicknames, reports, agent_prompt_history: promptHistory, summary, recent };
|
|
261
263
|
}
|
|
262
264
|
|
|
263
|
-
function
|
|
265
|
+
function slicePromptHistoryForProject(value = {}) {
|
|
266
|
+
const input = value && typeof value === "object" ? value : {};
|
|
267
|
+
const perAgent = Array.isArray(input.per_agent) ? input.per_agent.slice(0, 3) : [];
|
|
268
|
+
return {
|
|
269
|
+
scanned_files: Number(input.scanned_files || 0) || 0,
|
|
270
|
+
matched_events: Number(input.matched_events || 0) || 0,
|
|
271
|
+
per_agent: perAgent.map((row) => ({
|
|
272
|
+
agent_id: String(row && row.agent_id ? row.agent_id : ""),
|
|
273
|
+
nickname: String(row && row.nickname ? row.nickname : ""),
|
|
274
|
+
total_count: Number(row && row.total_count ? row.total_count : 0) || 0,
|
|
275
|
+
sample_count: Number(row && row.sample_count ? row.sample_count : 0) || 0,
|
|
276
|
+
last_ts: String(row && row.last_ts ? row.last_ts : ""),
|
|
277
|
+
samples: Array.isArray(row && row.samples)
|
|
278
|
+
? row.samples.slice(0, 2).map((sample) => ({
|
|
279
|
+
ts: String(sample && sample.ts ? sample.ts : ""),
|
|
280
|
+
publisher: String(sample && sample.publisher ? sample.publisher : ""),
|
|
281
|
+
prompt: String(sample && sample.prompt ? sample.prompt : ""),
|
|
282
|
+
}))
|
|
283
|
+
: [],
|
|
284
|
+
})),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function buildGlobalProjectRouterContext(projectRoot, options = {}) {
|
|
289
|
+
const maxProjects = Number.isFinite(options.maxProjects) && options.maxProjects > 0
|
|
290
|
+
? Math.floor(options.maxProjects)
|
|
291
|
+
: 12;
|
|
292
|
+
|
|
293
|
+
let rows = [];
|
|
294
|
+
try {
|
|
295
|
+
rows = listProjectRuntimes({ validate: true, cleanupTmp: true });
|
|
296
|
+
} catch {
|
|
297
|
+
rows = [];
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
rows = rows
|
|
301
|
+
.filter((row) => {
|
|
302
|
+
const status = String((row && row.status) || "").trim().toLowerCase();
|
|
303
|
+
if (status === "stopped") return false;
|
|
304
|
+
return !isGlobalControllerProjectRoot(row && row.project_root ? row.project_root : "");
|
|
305
|
+
})
|
|
306
|
+
.slice(0, maxProjects);
|
|
307
|
+
|
|
308
|
+
let activeAgentTotal = 0;
|
|
309
|
+
let busyAgentTotal = 0;
|
|
310
|
+
let unreadTotal = 0;
|
|
311
|
+
let decisionsOpenTotal = 0;
|
|
312
|
+
|
|
313
|
+
const projects = rows.map((row) => {
|
|
314
|
+
const targetRoot = String(row && row.project_root ? row.project_root : "");
|
|
315
|
+
const fallbackName = String(row && row.project_name ? row.project_name : targetRoot);
|
|
316
|
+
let topDirs = [];
|
|
317
|
+
try {
|
|
318
|
+
const entries = fs.readdirSync(targetRoot, { withFileTypes: true });
|
|
319
|
+
topDirs = entries
|
|
320
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules")
|
|
321
|
+
.map((e) => e.name)
|
|
322
|
+
.slice(0, 20);
|
|
323
|
+
} catch {
|
|
324
|
+
// ignore unreadable directories
|
|
325
|
+
}
|
|
326
|
+
try {
|
|
327
|
+
const status = buildStatus(targetRoot);
|
|
328
|
+
const activeMeta = Array.isArray(status && status.active_meta) ? status.active_meta : [];
|
|
329
|
+
const agents = activeMeta.map((item) => ({
|
|
330
|
+
id: String(item && item.id ? item.id : ""),
|
|
331
|
+
nickname: String(item && item.nickname ? item.nickname : ""),
|
|
332
|
+
display: String(item && item.display ? item.display : ""),
|
|
333
|
+
launch_mode: String(item && item.launch_mode ? item.launch_mode : ""),
|
|
334
|
+
activity_state: String(item && item.activity_state ? item.activity_state : ""),
|
|
335
|
+
activity_since: String(item && item.activity_since ? item.activity_since : ""),
|
|
336
|
+
}));
|
|
337
|
+
const nicknames = {};
|
|
338
|
+
agents.forEach((item) => {
|
|
339
|
+
if (item.nickname) nicknames[item.nickname] = item.id;
|
|
340
|
+
});
|
|
341
|
+
const promptHistory = buildAgentPromptHistory(targetRoot, agents, nicknames, {
|
|
342
|
+
perAgentLimit: 2,
|
|
343
|
+
maxFiles: 2,
|
|
344
|
+
});
|
|
345
|
+
const busyCount = agents.filter((item) => isBusyActivityState(item.activity_state)).length;
|
|
346
|
+
activeAgentTotal += agents.length;
|
|
347
|
+
busyAgentTotal += busyCount;
|
|
348
|
+
const unread = Number(status && status.unread && status.unread.total ? status.unread.total : 0) || 0;
|
|
349
|
+
const decisionsOpen = Number(status && status.decisions && status.decisions.open ? status.decisions.open : 0) || 0;
|
|
350
|
+
unreadTotal += unread;
|
|
351
|
+
decisionsOpenTotal += decisionsOpen;
|
|
352
|
+
return {
|
|
353
|
+
project_root: targetRoot,
|
|
354
|
+
project_name: fallbackName,
|
|
355
|
+
top_dirs: topDirs,
|
|
356
|
+
status: String(row && row.status ? row.status : "unknown"),
|
|
357
|
+
last_seen: String(row && row.last_seen ? row.last_seen : ""),
|
|
358
|
+
active_count: agents.length,
|
|
359
|
+
busy_count: busyCount,
|
|
360
|
+
ready_count: Math.max(agents.length - busyCount, 0),
|
|
361
|
+
unread_total: unread,
|
|
362
|
+
decisions_open: decisionsOpen,
|
|
363
|
+
reports_pending_total: Number(status && status.reports && status.reports.pending_total ? status.reports.pending_total : 0) || 0,
|
|
364
|
+
groups_active: Number(status && status.groups && status.groups.active ? status.groups.active : 0) || 0,
|
|
365
|
+
agents: agents.slice(0, 6),
|
|
366
|
+
agent_prompt_history: slicePromptHistoryForProject(promptHistory),
|
|
367
|
+
};
|
|
368
|
+
} catch {
|
|
369
|
+
return {
|
|
370
|
+
project_root: targetRoot,
|
|
371
|
+
project_name: fallbackName,
|
|
372
|
+
top_dirs: topDirs,
|
|
373
|
+
status: String(row && row.status ? row.status : "unknown"),
|
|
374
|
+
last_seen: String(row && row.last_seen ? row.last_seen : ""),
|
|
375
|
+
active_count: 0,
|
|
376
|
+
busy_count: 0,
|
|
377
|
+
ready_count: 0,
|
|
378
|
+
unread_total: 0,
|
|
379
|
+
decisions_open: 0,
|
|
380
|
+
reports_pending_total: 0,
|
|
381
|
+
groups_active: 0,
|
|
382
|
+
agents: [],
|
|
383
|
+
agent_prompt_history: { scanned_files: 0, matched_events: 0, per_agent: [] },
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
const runningCount = projects.filter((item) => item.status === "running").length;
|
|
389
|
+
const staleCount = projects.filter((item) => item.status === "stale").length;
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
mode: "global-router",
|
|
393
|
+
controller_project_root: projectRoot,
|
|
394
|
+
summary: {
|
|
395
|
+
project_count: projects.length,
|
|
396
|
+
running_count: runningCount,
|
|
397
|
+
stale_count: staleCount,
|
|
398
|
+
active_agent_total: activeAgentTotal,
|
|
399
|
+
busy_agent_total: busyAgentTotal,
|
|
400
|
+
unread_total: unreadTotal,
|
|
401
|
+
decisions_open_total: decisionsOpenTotal,
|
|
402
|
+
},
|
|
403
|
+
projects,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function buildSystemPrompt(context, options = {}) {
|
|
408
|
+
const mode = String(options.routingMode || (context && context.mode) || "").trim().toLowerCase();
|
|
409
|
+
if (mode === "global-router") {
|
|
410
|
+
return [
|
|
411
|
+
"You are ufoo-agent, the global project router for `ufoo chat -g`.",
|
|
412
|
+
"You run inside the home-scoped controller runtime and must choose the right project before any project-local routing happens.",
|
|
413
|
+
"Return ONLY valid JSON. No extra text.",
|
|
414
|
+
"Schema:",
|
|
415
|
+
"{",
|
|
416
|
+
' "reply": "string",',
|
|
417
|
+
` "assistant_call": {"kind":"explore|bash|mixed","task":"string","context":"optional","expect":"optional","provider":"codex|claude|ufoo (optional)","model":"optional","timeout_ms":${DEFAULT_ASSISTANT_TIMEOUT_MS}},`,
|
|
418
|
+
' "project_route": {"project_root":"absolute-path","project_name":"string","prompt":"string","reason":"string"},',
|
|
419
|
+
' "dispatch": [],',
|
|
420
|
+
' "ops": []',
|
|
421
|
+
"}",
|
|
422
|
+
"Rules:",
|
|
423
|
+
"- Use project_route when the request should be handed to one specific registered project.",
|
|
424
|
+
"- project_route.prompt should usually preserve the user request, optionally rewritten only to clarify project context for the next router.",
|
|
425
|
+
"- Each project entry has top_dirs: the immediate subdirectories of project_root. Use these to match sub-project or component names mentioned by the user (e.g. if user says 'voyager' and a project has 'voyager' in top_dirs, route there).",
|
|
426
|
+
"- Keep dispatch empty in global-router mode. Do NOT send directly to coding agents from the global controller.",
|
|
427
|
+
"- Keep ops empty in global-router mode. Do NOT launch/rename/close/cron project-local agents from the global controller.",
|
|
428
|
+
"- The target project's ufoo-agent will do the second-hop routing to a concrete agent.",
|
|
429
|
+
"- If the user asks for a global comparison, registry overview, or other controller-level answer, reply directly and omit project_route.",
|
|
430
|
+
"- If no registered project is a clear match, reply with a concise clarification request or tell the user to use /open <path> first.",
|
|
431
|
+
"- assistant_call is allowed for lightweight controller-side inspection when the registry/context is insufficient.",
|
|
432
|
+
"- Prefer continuity: if a project's recent prompt history clearly matches the current request, route there.",
|
|
433
|
+
"",
|
|
434
|
+
"Context: registered projects and project activity summaries:",
|
|
435
|
+
JSON.stringify(context),
|
|
436
|
+
].join("\n");
|
|
437
|
+
}
|
|
438
|
+
|
|
264
439
|
const hasAgents = context.agents && context.agents.length > 0;
|
|
265
440
|
const agentGuidance = hasAgents
|
|
266
441
|
? ""
|
|
@@ -275,7 +450,7 @@ function buildSystemPrompt(context) {
|
|
|
275
450
|
' "reply": "string",',
|
|
276
451
|
` "assistant_call": {"kind":"explore|bash|mixed","task":"string","context":"optional","expect":"optional","provider":"codex|claude|ufoo (optional)","model":"optional","timeout_ms":${DEFAULT_ASSISTANT_TIMEOUT_MS}},`,
|
|
277
452
|
' "dispatch": [{"target":"broadcast|<agent-id>|<nickname>","message":"string","injection_mode":"immediate|queued (optional)","source":"optional"}],',
|
|
278
|
-
' "ops": [{"action":"launch|close|rename|cron","agent":"codex|claude|ucode","count":1,"agent_id":"id","nickname":"optional","operation":"start|list|stop","every":"30m","interval_ms":1800000,"target":"agent-id|nickname|csv","targets":["agent-id"],"prompt":"message","id":"task-id|all"}],',
|
|
453
|
+
' "ops": [{"action":"launch|close|rename|role|cron","agent":"codex|claude|ucode","count":1,"agent_id":"id","nickname":"optional","prompt_profile":"profile-id (for role)","operation":"start|list|stop","every":"30m","interval_ms":1800000,"at":"YYYY-MM-DD HH:mm","once_at_ms":1700000000000,"target":"agent-id|nickname|csv","targets":["agent-id"],"title":"optional short title","prompt":"message","id":"task-id|all"}],',
|
|
279
454
|
' "disambiguate": {"prompt":"string","candidates":[{"agent_id":"id","reason":"string"}]}',
|
|
280
455
|
"}",
|
|
281
456
|
"Rules:",
|
|
@@ -283,9 +458,10 @@ function buildSystemPrompt(context) {
|
|
|
283
458
|
"- If multiple possible agents, use disambiguate with candidates and no dispatch.",
|
|
284
459
|
"- If user specifies a nickname for a new agent, include ops.launch with nickname so daemon can rename.",
|
|
285
460
|
"- If user requests rename, use ops.rename with agent_id and nickname (do NOT launch).",
|
|
286
|
-
"- For scheduled follow-up (cron), use ops.cron with operation=start and include
|
|
461
|
+
"- For scheduled follow-up (cron), use ops.cron with operation=start and include target(s)+prompt, plus optional title; use every/interval_ms for recurring or at/once_at_ms for one-time.",
|
|
287
462
|
"- To check scheduled tasks, use ops.cron with operation=list.",
|
|
288
463
|
"- To stop scheduled tasks, use ops.cron with operation=stop and id (or id=all).",
|
|
464
|
+
"- To assign a preset role to an existing agent, use ops.role with target (agent-id or nickname) and prompt_profile (profile id or alias). Available profiles: discovery-facilitator, scope-challenger, system-architect, implementation-lead, frontend-refiner, design-critic, review-critic, qa-driver, debug-investigator, release-coordinator, task-breakdown, research-scan, rapid-prototype.",
|
|
289
465
|
"- Use top-level assistant_call for project exploration, temporary shell tasks, and quick execution support.",
|
|
290
466
|
"- assistant_call fields: kind (explore|bash|mixed), task (required), context/expect (optional), provider (codex|claude|ufoo, optional), model/timeout_ms (optional).",
|
|
291
467
|
"- Prefer assistant_call over launching coding agents when the task is short-lived.",
|
|
@@ -453,10 +629,13 @@ async function runNativeRouterCall({ projectRoot, prompt, systemPrompt, model: r
|
|
|
453
629
|
}
|
|
454
630
|
}
|
|
455
631
|
|
|
456
|
-
async function runUfooAgent({ projectRoot, prompt, provider, model }) {
|
|
632
|
+
async function runUfooAgent({ projectRoot, prompt, provider, model, routingMode = "", routingContext = null }) {
|
|
457
633
|
const state = loadSessionState(projectRoot);
|
|
458
|
-
const
|
|
459
|
-
const
|
|
634
|
+
const mode = String(routingMode || (routingContext && routingContext.mode) || "").trim().toLowerCase();
|
|
635
|
+
const bus = routingContext || (mode === "global-router"
|
|
636
|
+
? buildGlobalProjectRouterContext(projectRoot)
|
|
637
|
+
: loadBusSummary(projectRoot));
|
|
638
|
+
const systemPrompt = buildSystemPrompt(bus, { routingMode: mode });
|
|
460
639
|
const history = loadHistory(projectRoot);
|
|
461
640
|
const historyPrompt = buildHistoryPrompt(history);
|
|
462
641
|
const fullPrompt = historyPrompt ? `${historyPrompt}User: ${prompt}` : prompt;
|
package/src/assistant/engine.js
CHANGED
|
@@ -18,16 +18,11 @@ function resolveAssistantEngine({
|
|
|
18
18
|
} = {}) {
|
|
19
19
|
const config = loadConfig(projectRoot);
|
|
20
20
|
|
|
21
|
-
const hasRequestedProvider = String(requestedProvider || "").trim().length > 0;
|
|
22
21
|
const requested = normalizeAssistantEngine(requestedProvider);
|
|
23
|
-
const configEngine = normalizeAssistantEngine(config.assistantEngine);
|
|
24
22
|
const fallback = normalizeAssistantEngine(fallbackProvider) || "codex";
|
|
25
23
|
|
|
26
24
|
let selected = requested;
|
|
27
|
-
|
|
28
|
-
// Explicit assistant_call provider=auto should inherit current main agent provider.
|
|
29
|
-
selected = hasRequestedProvider ? fallback : configEngine;
|
|
30
|
-
}
|
|
25
|
+
// Omitted/auto assistant providers inherit the active ufoo-agent provider.
|
|
31
26
|
if (selected === "auto") selected = fallback;
|
|
32
27
|
if (selected === "auto") selected = "codex";
|
|
33
28
|
|