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
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,"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"}],',
|
|
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:",
|
|
@@ -286,6 +461,7 @@ function buildSystemPrompt(context) {
|
|
|
286
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;
|
|
@@ -6,6 +6,8 @@ const { runGroupCoreCommand } = require("../cli/groupCoreCommands");
|
|
|
6
6
|
const { loadConfig: loadProjectConfig, saveConfig: saveProjectConfig, loadGlobalUcodeConfig, saveGlobalUcodeConfig } = require("../config");
|
|
7
7
|
const { resolveTransport } = require("../code/nativeRunner");
|
|
8
8
|
const { parseIntervalMs, formatIntervalMs } = require("./cronScheduler");
|
|
9
|
+
const { isGlobalControllerProjectRoot, resolveGlobalControllerUfooDir } = require("../globalMode");
|
|
10
|
+
const { loadPromptProfileRegistry } = require("../group/promptProfiles");
|
|
9
11
|
|
|
10
12
|
function defaultCreateDoctor(projectRoot) {
|
|
11
13
|
const UfooDoctor = require("../doctor");
|
|
@@ -92,6 +94,7 @@ function createCommandExecutor(options = {}) {
|
|
|
92
94
|
stopCronTask = () => false,
|
|
93
95
|
runGroupCore = runGroupCoreCommand,
|
|
94
96
|
requestCron = null,
|
|
97
|
+
globalMode = false,
|
|
95
98
|
listProjects = () => [],
|
|
96
99
|
getCurrentProject = () => ({ projectRoot }),
|
|
97
100
|
switchProject = async () => ({ ok: false, error: "project switching unavailable" }),
|
|
@@ -404,7 +407,7 @@ function createCommandExecutor(options = {}) {
|
|
|
404
407
|
if (args.length === 0) {
|
|
405
408
|
logMessage(
|
|
406
409
|
"error",
|
|
407
|
-
"{white-fg}✗{/white-fg} Usage: /launch <claude|codex|ucode> [nickname=<name>] [count=<n>] [scope=inplace|window]"
|
|
410
|
+
"{white-fg}✗{/white-fg} Usage: /launch <claude|codex|ucode> [nickname=<name>] [profile=<id>] [count=<n>] [scope=inplace|window]"
|
|
408
411
|
);
|
|
409
412
|
return;
|
|
410
413
|
}
|
|
@@ -452,6 +455,7 @@ function createCommandExecutor(options = {}) {
|
|
|
452
455
|
}
|
|
453
456
|
|
|
454
457
|
const nickname = parsedOptions.nickname || "";
|
|
458
|
+
const promptProfile = parsedOptions.profile || parsedOptions.prompt_profile || "";
|
|
455
459
|
const count = parseInt(parsedOptions.count || "1", 10);
|
|
456
460
|
const scopeRaw = parsedOptions.scope || parsedOptions.launch_scope || parsedOptions.window || "";
|
|
457
461
|
let launchScope = normalizeLaunchScopeOption(scopeRaw, "inplace");
|
|
@@ -473,6 +477,10 @@ function createCommandExecutor(options = {}) {
|
|
|
473
477
|
logMessage("error", "{white-fg}✗{/white-fg} nickname requires count=1");
|
|
474
478
|
return;
|
|
475
479
|
}
|
|
480
|
+
if (promptProfile && count > 1) {
|
|
481
|
+
logMessage("error", "{white-fg}✗{/white-fg} profile requires count=1");
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
476
484
|
|
|
477
485
|
try {
|
|
478
486
|
const request = {
|
|
@@ -480,6 +488,7 @@ function createCommandExecutor(options = {}) {
|
|
|
480
488
|
agent: normalizedAgent,
|
|
481
489
|
count: Number.isFinite(count) ? count : 1,
|
|
482
490
|
nickname,
|
|
491
|
+
prompt_profile: promptProfile,
|
|
483
492
|
launch_scope: launchScope,
|
|
484
493
|
...collectHostLaunchRequestContext(),
|
|
485
494
|
};
|
|
@@ -494,6 +503,52 @@ function createCommandExecutor(options = {}) {
|
|
|
494
503
|
}
|
|
495
504
|
}
|
|
496
505
|
|
|
506
|
+
async function handleRoleCommand(args = []) {
|
|
507
|
+
const action = String(args[0] || "").trim().toLowerCase();
|
|
508
|
+
if (action === "list" || action === "ls") {
|
|
509
|
+
try {
|
|
510
|
+
const registry = loadPromptProfileRegistry(projectRoot);
|
|
511
|
+
const profiles = registry.profiles || [];
|
|
512
|
+
if (profiles.length === 0) {
|
|
513
|
+
logMessage("system", "{white-fg}⚙{/white-fg} No prompt profiles found.");
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
logMessage("system", `{white-fg}⚙{/white-fg} Available prompt profiles (${profiles.length}):`);
|
|
517
|
+
for (const p of profiles) {
|
|
518
|
+
const aliases = p.aliases && p.aliases.length > 0 ? ` {gray-fg}(${p.aliases.join(", ")}){/gray-fg}` : "";
|
|
519
|
+
const source = p.source ? ` {cyan-fg}[${p.source}]{/cyan-fg}` : "";
|
|
520
|
+
const summary = p.summary ? ` ${p.summary}` : "";
|
|
521
|
+
logMessage("system", ` {bold}${escapeBlessed(p.id)}{/bold}${aliases}${source}`);
|
|
522
|
+
if (summary) {
|
|
523
|
+
logMessage("system", ` ${escapeBlessed(summary)}`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
} catch (err) {
|
|
527
|
+
logMessage("error", `{white-fg}✗{/white-fg} Failed to list profiles: ${escapeBlessed(err.message)}`);
|
|
528
|
+
}
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const target = String(args[0] || "").trim();
|
|
533
|
+
const profile = String(args[1] || "").trim();
|
|
534
|
+
if (!target || !profile) {
|
|
535
|
+
logMessage("error", "{white-fg}✗{/white-fg} Usage: /role <agent-id|nickname> <prompt-profile>");
|
|
536
|
+
logMessage("error", " /role list");
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
try {
|
|
541
|
+
send({
|
|
542
|
+
type: IPC_REQUEST_TYPES.ASSIGN_ROLE,
|
|
543
|
+
target,
|
|
544
|
+
prompt_profile: profile,
|
|
545
|
+
});
|
|
546
|
+
schedule(requestStatus, 1000);
|
|
547
|
+
} catch (err) {
|
|
548
|
+
logMessage("error", `{white-fg}✗{/white-fg} Role assignment failed: ${escapeBlessed(err.message)}`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
497
552
|
async function handleResumeCommand(args = []) {
|
|
498
553
|
const action = String(args[0] || "").toLowerCase();
|
|
499
554
|
if (action === "list" || action === "ls") {
|
|
@@ -517,7 +572,11 @@ function createCommandExecutor(options = {}) {
|
|
|
517
572
|
|
|
518
573
|
if (subcommand === "list") {
|
|
519
574
|
const rowsRaw = await Promise.resolve(listProjects());
|
|
520
|
-
const rows = Array.isArray(rowsRaw) ? rowsRaw : []
|
|
575
|
+
const rows = (Array.isArray(rowsRaw) ? rowsRaw : []).filter((row) => {
|
|
576
|
+
if (!globalMode) return true;
|
|
577
|
+
const root = row && row.project_root ? String(row.project_root) : "";
|
|
578
|
+
return !isGlobalControllerProjectRoot(root);
|
|
579
|
+
});
|
|
521
580
|
const current = await Promise.resolve(getCurrentProject());
|
|
522
581
|
const currentRoot = current && current.project_root ? String(current.project_root) : "";
|
|
523
582
|
if (rows.length === 0) {
|
|
@@ -545,12 +604,19 @@ function createCommandExecutor(options = {}) {
|
|
|
545
604
|
logMessage("error", "{white-fg}✗{/white-fg} Current project unavailable");
|
|
546
605
|
return;
|
|
547
606
|
}
|
|
607
|
+
if (globalMode && isGlobalControllerProjectRoot(current.project_root)) {
|
|
608
|
+
logMessage(
|
|
609
|
+
"system",
|
|
610
|
+
`{cyan-fg}Current:{/cyan-fg} global controller (${escapeBlessed(resolveGlobalControllerUfooDir())})`
|
|
611
|
+
);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
548
614
|
logMessage("system", `{cyan-fg}Current:{/cyan-fg} ${escapeBlessed(current.project_root)}`);
|
|
549
615
|
return;
|
|
550
616
|
}
|
|
551
617
|
|
|
552
618
|
if (subcommand === "switch") {
|
|
553
|
-
const target =
|
|
619
|
+
const target = args.slice(1).join(" ").trim();
|
|
554
620
|
if (!target) {
|
|
555
621
|
logMessage("error", "{white-fg}✗{/white-fg} Usage: /project switch <index|path>");
|
|
556
622
|
return;
|
|
@@ -570,6 +636,27 @@ function createCommandExecutor(options = {}) {
|
|
|
570
636
|
logMessage("error", "{white-fg}✗{/white-fg} Unknown project command. Use: list, current, switch");
|
|
571
637
|
}
|
|
572
638
|
|
|
639
|
+
async function handleOpenCommand(args = []) {
|
|
640
|
+
if (!globalMode) {
|
|
641
|
+
logMessage("error", "{white-fg}✗{/white-fg} /open is only available in global mode");
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const target = args.join(" ").trim();
|
|
645
|
+
if (!target) {
|
|
646
|
+
logMessage("error", "{white-fg}✗{/white-fg} Usage: /open <path>");
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
logMessage("system", `{white-fg}⚙{/white-fg} Opening project: ${escapeBlessed(target)}`);
|
|
650
|
+
const result = await Promise.resolve(switchProject({ target }));
|
|
651
|
+
if (!result || result.ok !== true) {
|
|
652
|
+
const reason = result && result.error ? String(result.error) : "open failed";
|
|
653
|
+
logMessage("error", `{white-fg}✗{/white-fg} Open failed: ${escapeBlessed(reason)}`);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const nextRoot = result.project_root || result.projectRoot || "";
|
|
657
|
+
logMessage("system", `{white-fg}✓{/white-fg} Opened project: ${escapeBlessed(nextRoot)}`);
|
|
658
|
+
}
|
|
659
|
+
|
|
573
660
|
function parseKeyValueArgs(args = []) {
|
|
574
661
|
const parsed = {};
|
|
575
662
|
for (const raw of args) {
|
|
@@ -1120,12 +1207,18 @@ function createCommandExecutor(options = {}) {
|
|
|
1120
1207
|
case "launch":
|
|
1121
1208
|
await handleLaunchCommand(args);
|
|
1122
1209
|
return true;
|
|
1210
|
+
case "open":
|
|
1211
|
+
await handleOpenCommand(args);
|
|
1212
|
+
return true;
|
|
1123
1213
|
case "resume":
|
|
1124
1214
|
await handleResumeCommand(args);
|
|
1125
1215
|
return true;
|
|
1126
1216
|
case "project":
|
|
1127
1217
|
await handleProjectCommand(args);
|
|
1128
1218
|
return true;
|
|
1219
|
+
case "role":
|
|
1220
|
+
await handleRoleCommand(args);
|
|
1221
|
+
return true;
|
|
1129
1222
|
case "cron":
|
|
1130
1223
|
await handleCronCommand(args);
|
|
1131
1224
|
return true;
|
|
@@ -1154,8 +1247,10 @@ function createCommandExecutor(options = {}) {
|
|
|
1154
1247
|
handleCtxCommand,
|
|
1155
1248
|
handleSkillsCommand,
|
|
1156
1249
|
handleLaunchCommand,
|
|
1250
|
+
handleOpenCommand,
|
|
1157
1251
|
handleResumeCommand,
|
|
1158
1252
|
handleProjectCommand,
|
|
1253
|
+
handleRoleCommand,
|
|
1159
1254
|
handleCronCommand,
|
|
1160
1255
|
handleGroupCommand,
|
|
1161
1256
|
handleSettingsCommand,
|
package/src/chat/commands.js
CHANGED
|
@@ -47,6 +47,7 @@ const COMMAND_TREE = {
|
|
|
47
47
|
},
|
|
48
48
|
},
|
|
49
49
|
"/init": { desc: "Initialize modules" },
|
|
50
|
+
"/open": { desc: "Open project path in global mode" },
|
|
50
51
|
"/launch": {
|
|
51
52
|
desc: "Launch new agent",
|
|
52
53
|
children: {
|
|
@@ -63,6 +64,12 @@ const COMMAND_TREE = {
|
|
|
63
64
|
switch: { desc: "Switch daemon connection to project index/path" },
|
|
64
65
|
},
|
|
65
66
|
},
|
|
67
|
+
"/role": {
|
|
68
|
+
desc: "Assign preset role to an existing agent",
|
|
69
|
+
children: {
|
|
70
|
+
list: { desc: "List available prompt profiles" },
|
|
71
|
+
},
|
|
72
|
+
},
|
|
66
73
|
"/resume": {
|
|
67
74
|
desc: "Resume agents (optional nickname) or list recoverable targets",
|
|
68
75
|
children: {
|