u-foo 3.0.10 → 3.0.12
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 +25 -9
- package/README.zh-CN.md +22 -9
- package/SKILLS/ubus/SKILL.md +15 -9
- package/SKILLS/ufoo/SKILL.md +20 -6
- package/dist/tui/darwin-arm64/ufoo-tui +0 -0
- package/dist/tui/darwin-x64/ufoo-tui +0 -0
- package/dist/tui/linux-arm64/ufoo-tui +0 -0
- package/dist/tui/linux-x64/ufoo-tui +0 -0
- package/package.json +11 -3
- package/scripts/pack-tui.js +112 -0
- package/scripts/postinstall.js +11 -0
- package/src/agents/activity/activityReconcile.js +106 -0
- package/src/agents/activity/activityStatePublisher.js +31 -2
- package/src/agents/activity/index.js +1 -0
- package/src/agents/launch/launcher.js +19 -0
- package/src/agents/launch/ptyRunner.js +20 -1
- package/src/agents/prompts/groupBootstrap.js +4 -0
- package/src/agents/prompts/native/ufoo.js +2 -0
- package/src/app/chat/ChatController.js +433 -0
- package/src/app/chat/agentDirectory.js +63 -0
- package/src/app/chat/agentEnter.js +70 -0
- package/src/app/chat/agentIdentity.js +50 -0
- package/src/app/chat/bootstrap.js +66 -0
- package/src/app/chat/commandExecutor.js +108 -0
- package/src/app/chat/commands.js +31 -0
- package/src/app/chat/dashboardView.js +6 -2
- package/src/app/chat/historyStore.js +181 -0
- package/src/app/chat/index.js +14 -2
- package/src/app/chat/inputSubmitHandler.js +21 -7
- package/src/app/chat/ipcBuilders.js +52 -0
- package/src/app/chat/multiWindow/paneManager.js +10 -1
- package/src/app/chat/multiWindow/renderer.js +1 -1
- package/src/app/chat/multiWindow/vtFrame.js +93 -0
- package/src/app/chat/streamState.js +182 -0
- package/src/app/cli/features/doctor.js +22 -0
- package/src/code/UCODE_PROMPT.md +2 -0
- package/src/code/UcodeController.js +156 -0
- package/src/code/context/planGraphService.js +4 -0
- package/src/code/repl.js +4 -3
- package/src/code/runtime/taskLoop.js +46 -50
- package/src/code/tui.js +13 -2
- package/src/code/ucodeSlashDispatch.js +241 -0
- package/src/coordination/bus/activate.js +3 -0
- package/src/runtime/contracts/schemas/ufoo-ui-v1/envelope.json +28 -0
- package/src/runtime/contracts/uiProtocol.js +190 -0
- package/src/ui/{ink/chatLogModel.js → chatLogModel.js} +2 -2
- package/src/ui/dashboardBridge.js +81 -0
- package/src/ui/format/index.js +2 -2
- package/src/ui/index.js +8 -4
- package/src/ui/multiPaneBusMirror.js +137 -0
- package/src/ui/multiWindowHandoff.js +232 -0
- package/src/ui/ptyHandoff.js +23 -0
- package/src/ui/rustChatHost.js +1520 -0
- package/src/ui/rustMultiSession.js +497 -0
- package/src/ui/rustUcodeHost.js +999 -0
- package/src/ui/scrollbackReplay.js +82 -0
- package/src/ui/settingsBridge.js +49 -0
- package/src/ui/toolMergeBridge.js +66 -0
- package/src/ui/tuiLauncher.js +105 -0
- package/src/ui/ucodeStatusLine.js +74 -0
- package/src/ui/uiHostServer.js +339 -0
- package/src/ui/MIGRATION.md +0 -334
- package/src/ui/ink/ChatApp.js +0 -4163
- package/src/ui/ink/DashboardBar.js +0 -691
- package/src/ui/ink/InkDemo.js +0 -96
- package/src/ui/ink/MultilineInput.js +0 -662
- package/src/ui/ink/UcodeApp.js +0 -1675
- package/src/ui/ink/agentMirror.js +0 -730
- package/src/ui/ink/chatReducer.js +0 -473
- package/src/ui/runInk.js +0 -66
package/README.md
CHANGED
|
@@ -113,7 +113,7 @@ per-project MCP server mode.
|
|
|
113
113
|
|
|
114
114
|
```text
|
|
115
115
|
ufoo / ufoo chat
|
|
116
|
-
-> src/app/chat + src/ui/
|
|
116
|
+
-> src/app/chat + src/ui/rustChatHost + crates/ufoo-tui
|
|
117
117
|
-> project daemon over .ufoo/run/ufoo.sock
|
|
118
118
|
-> runtime daemon launch/resume/recover/reports/cron/groups
|
|
119
119
|
-> orchestration router, group templates, solo roles
|
|
@@ -314,7 +314,8 @@ Global `ucode` settings:
|
|
|
314
314
|
```text
|
|
315
315
|
src/
|
|
316
316
|
app/ chat client state and CLI command entry
|
|
317
|
-
ui/
|
|
317
|
+
ui/ Rust TUI hosts + pure formatting helpers
|
|
318
|
+
crates/ufoo-tui Rust ratatui child (required for chat/ucode)
|
|
318
319
|
runtime/ daemon, projects, terminal adapters, contracts, privacy, process helpers
|
|
319
320
|
coordination/ bus, context, memory, history, reports, state, status
|
|
320
321
|
orchestration/ router/controller logic, groups, solo roles
|
|
@@ -343,23 +344,38 @@ npm run test:watch
|
|
|
343
344
|
npm run test:coverage
|
|
344
345
|
```
|
|
345
346
|
|
|
346
|
-
The repository is CommonJS, targets Node.js 18+, and
|
|
347
|
+
The repository is CommonJS, targets Node.js 18+, and ships a Rust TTY UI
|
|
348
|
+
(`crates/ufoo-tui`) as platform binaries under `dist/tui/`.
|
|
347
349
|
|
|
348
350
|
## Release
|
|
349
351
|
|
|
350
|
-
|
|
352
|
+
Releases are published by GitHub Actions (`.github/workflows/release.yml`).
|
|
353
|
+
The workflow builds `ufoo-tui` for `darwin-arm64`, `darwin-x64`, `linux-x64`,
|
|
354
|
+
and `linux-arm64`, stages them under `dist/tui/<platform>/`, then runs
|
|
355
|
+
`npm publish`.
|
|
356
|
+
|
|
357
|
+
1. Bump the version and push a matching tag:
|
|
351
358
|
|
|
352
359
|
```bash
|
|
353
360
|
npm test
|
|
354
|
-
npm
|
|
355
|
-
npm version patch
|
|
356
|
-
npm publish --access public
|
|
361
|
+
npm version patch # commits package.json + creates tag vX.Y.Z
|
|
357
362
|
git push --follow-tags
|
|
358
363
|
```
|
|
359
364
|
|
|
360
|
-
|
|
361
|
-
|
|
365
|
+
2. Ensure the repo secret `NPM_TOKEN` is set (npm automation token with
|
|
366
|
+
publish rights for `u-foo`).
|
|
367
|
+
3. The `Release` workflow runs on the `v*` tag. Use **Actions → Release →
|
|
368
|
+
Run workflow** for a manual dry-run (`dry_run=true` packs but does not publish).
|
|
369
|
+
|
|
370
|
+
Local one-platform staging (dev only):
|
|
371
|
+
|
|
372
|
+
```bash
|
|
373
|
+
npm run pack:tui # cargo build + copy into dist/tui/$PLATFORM
|
|
374
|
+
npm pack --dry-run # requires at least one staged binary (prepack check)
|
|
375
|
+
```
|
|
362
376
|
|
|
377
|
+
Publishing without the GitHub workflow still requires an npm account/token with
|
|
378
|
+
permission for `u-foo`, plus staged `dist/tui/` binaries.
|
|
363
379
|
## Troubleshooting
|
|
364
380
|
|
|
365
381
|
Run a local entry directly if the linked binary is not on `PATH`:
|
package/README.zh-CN.md
CHANGED
|
@@ -110,7 +110,7 @@ MCP bridge 会连接 home 级 global controller daemon,并通过全局项目 r
|
|
|
110
110
|
|
|
111
111
|
```text
|
|
112
112
|
ufoo / ufoo chat
|
|
113
|
-
-> src/app/chat + src/ui/
|
|
113
|
+
-> src/app/chat + src/ui/rustChatHost + crates/ufoo-tui
|
|
114
114
|
-> project daemon over .ufoo/run/ufoo.sock
|
|
115
115
|
-> runtime daemon launch/resume/recover/reports/cron/groups
|
|
116
116
|
-> orchestration router, group templates, solo roles
|
|
@@ -306,7 +306,8 @@ ufoo ucode build
|
|
|
306
306
|
```text
|
|
307
307
|
src/
|
|
308
308
|
app/ chat client state 和 CLI command entry
|
|
309
|
-
ui/
|
|
309
|
+
ui/ Rust TUI hosts + 纯格式化 helper
|
|
310
|
+
crates/ufoo-tui Rust ratatui 子进程(chat/ucode 必需)
|
|
310
311
|
runtime/ daemon、projects、terminal adapters、contracts、privacy、process helpers
|
|
311
312
|
coordination/ bus、context、memory、history、reports、state、status
|
|
312
313
|
orchestration/ router/controller logic、groups、solo roles
|
|
@@ -336,22 +337,34 @@ npm run test:coverage
|
|
|
336
337
|
npm run bench:global-switch
|
|
337
338
|
```
|
|
338
339
|
|
|
339
|
-
本仓库是 CommonJS,目标 Node.js 18
|
|
340
|
+
本仓库是 CommonJS,目标 Node.js 18+,并通过 `dist/tui/` 附带 Rust TTY UI
|
|
341
|
+
(`crates/ufoo-tui`)的平台二进制。
|
|
340
342
|
|
|
341
343
|
## 发布
|
|
342
344
|
|
|
343
|
-
|
|
345
|
+
发布由 GitHub Actions(`.github/workflows/release.yml`)完成:多平台编译
|
|
346
|
+
`ufoo-tui`(`darwin-arm64` / `darwin-x64` / `linux-x64` / `linux-arm64`),
|
|
347
|
+
写入 `dist/tui/<platform>/`,再执行 `npm publish`。
|
|
348
|
+
|
|
349
|
+
1. 升版本并推送对应 tag:
|
|
344
350
|
|
|
345
351
|
```bash
|
|
346
352
|
npm test
|
|
347
|
-
npm
|
|
348
|
-
npm version patch
|
|
349
|
-
npm publish --access public
|
|
353
|
+
npm version patch # 提交 package.json 并创建 tag vX.Y.Z
|
|
350
354
|
git push --follow-tags
|
|
351
355
|
```
|
|
352
356
|
|
|
353
|
-
|
|
354
|
-
|
|
357
|
+
2. 仓库需配置 secret `NPM_TOKEN`(对 `u-foo` 有发布权限的 npm automation token)。
|
|
358
|
+
3. 推送 `v*` tag 后自动跑 `Release` workflow。也可在 Actions 里手动
|
|
359
|
+
**Run workflow**(`dry_run=true` 只打包不发布)。
|
|
360
|
+
|
|
361
|
+
本地单平台预演:
|
|
362
|
+
|
|
363
|
+
```bash
|
|
364
|
+
npm run pack:tui
|
|
365
|
+
npm pack --dry-run
|
|
366
|
+
```
|
|
367
|
+
|
|
355
368
|
|
|
356
369
|
## 故障排查
|
|
357
370
|
|
package/SKILLS/ubus/SKILL.md
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ubus
|
|
3
3
|
description: |
|
|
4
|
-
|
|
5
|
-
Use when: (1)
|
|
4
|
+
Check and handle pending event-bus messages when /ubus is explicitly invoked.
|
|
5
|
+
Use when: (1) asked to check messages, (2) view bus status, (3) use watch/listen/auto modes.
|
|
6
6
|
If not yet joined bus, will auto-join.
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
# /ubus - Event Bus
|
|
9
|
+
# /ubus - Check Event Bus Messages
|
|
10
10
|
|
|
11
|
-
Check pending messages on the event bus
|
|
11
|
+
Check and handle pending messages on the event bus when `/ubus` is explicitly
|
|
12
|
+
invoked.
|
|
12
13
|
|
|
13
14
|
## Arguments
|
|
14
15
|
|
|
15
|
-
- `/ubus` -
|
|
16
|
+
- `/ubus` - Pull pending messages and show status
|
|
16
17
|
- `/ubus watch` - Start background auto-notification (title badge + bell + notification center)
|
|
17
18
|
- `/ubus stop` - Stop background auto-notification
|
|
18
19
|
- `/ubus listen` - Foreground continuous listener, print new messages (suitable for side terminal)
|
|
@@ -74,7 +75,7 @@ If argument is `listen`, foreground blocking listener (no background task tool n
|
|
|
74
75
|
ufoo bus listen "$SUBSCRIBER" --from-beginning
|
|
75
76
|
```
|
|
76
77
|
|
|
77
|
-
If argument is `auto`, unattended auto-execute:
|
|
78
|
+
If argument is `auto`, use unattended auto-execute:
|
|
78
79
|
|
|
79
80
|
```bash
|
|
80
81
|
# Start daemon (background resident), auto-inject /ubus + Enter on new message
|
|
@@ -118,7 +119,8 @@ After you have read and processed the messages, you MUST acknowledge them to pre
|
|
|
118
119
|
ufoo bus ack "$SUBSCRIBER"
|
|
119
120
|
```
|
|
120
121
|
|
|
121
|
-
**This is critical** - if you don't ack, the
|
|
122
|
+
**This is critical** - if you don't ack, the runtime may retry delivery or keep
|
|
123
|
+
the event pending.
|
|
122
124
|
|
|
123
125
|
**Default behavior is ack-only, no reply.** If there's nothing to do (no actionable task, no question to answer, no follow-up the sender genuinely needs), just ack and stop. Silence is a valid response — see "Handling Received Messages" below for when a reply IS warranted.
|
|
124
126
|
|
|
@@ -196,11 +198,15 @@ and the two of you will ping-pong forever.
|
|
|
196
198
|
| `这个 bug 的根因是什么?` (question) | ✅ reply with answer |
|
|
197
199
|
| `我帮你找到了 X,需要你做 Y` (request) | ✅ reply with status |
|
|
198
200
|
|
|
199
|
-
When in doubt: ack and
|
|
200
|
-
|
|
201
|
+
When in doubt: ack and stop. If the sender genuinely needs something from you,
|
|
202
|
+
they will follow up with a concrete question or task.
|
|
201
203
|
|
|
202
204
|
## Sending Messages
|
|
203
205
|
|
|
206
|
+
After sending a message, do not run `/ubus`, poll, sleep, or wait for a reply.
|
|
207
|
+
Continue the current task. Any follow-up message will be automatically injected
|
|
208
|
+
into your prompt/session.
|
|
209
|
+
|
|
204
210
|
### Smart Routing (when you don't know the target ID)
|
|
205
211
|
|
|
206
212
|
If the user says "notify codex to do X" without specifying an ID, use smart routing:
|
package/SKILLS/ufoo/SKILL.md
CHANGED
|
@@ -117,11 +117,21 @@ ufoo memory audit mem-0001
|
|
|
117
117
|
|
|
118
118
|
## 3. Event Bus (ubus)
|
|
119
119
|
|
|
120
|
+
### Delivery Rule
|
|
121
|
+
|
|
122
|
+
After `ufoo bus send` or `ufoo bus broadcast`, do not run
|
|
123
|
+
`ufoo bus check`, invoke `/ubus`, sleep, or wait for a reply. Continue the
|
|
124
|
+
current task. Any follow-up message will be automatically injected into your
|
|
125
|
+
prompt/session.
|
|
126
|
+
|
|
127
|
+
When `/ubus` is explicitly requested, follow its pending-message flow
|
|
128
|
+
immediately. Do not invoke it merely to wait for a reply to a message you sent.
|
|
129
|
+
|
|
120
130
|
### Commands
|
|
121
131
|
|
|
122
132
|
```bash
|
|
123
|
-
ufoo bus check "$UFOO_SUBSCRIBER_ID" #
|
|
124
|
-
ufoo bus ack "$UFOO_SUBSCRIBER_ID"
|
|
133
|
+
ufoo bus check "$UFOO_SUBSCRIBER_ID" # Used by an explicit /ubus request
|
|
134
|
+
ufoo bus ack "$UFOO_SUBSCRIBER_ID" # Acknowledge after handling
|
|
125
135
|
ufoo bus send "<target>" "<message>" # Send message
|
|
126
136
|
ufoo bus broadcast "<message>" # Broadcast to all
|
|
127
137
|
ufoo bus status # Show bus status
|
|
@@ -149,13 +159,13 @@ Notes:
|
|
|
149
159
|
- Type: `codex` (all codex agents)
|
|
150
160
|
- Wildcard: `*` (broadcast)
|
|
151
161
|
|
|
152
|
-
### CRITICAL: When
|
|
162
|
+
### CRITICAL: When a message is received
|
|
153
163
|
|
|
154
164
|
**EXECUTE tasks immediately. Do NOT ask the user.**
|
|
155
165
|
|
|
156
|
-
1.
|
|
157
|
-
2. Execute each task
|
|
158
|
-
3. **Always ack**: `ufoo bus ack $UFOO_SUBSCRIBER_ID`
|
|
166
|
+
1. Read the available `[ufoo]<from:...>` message.
|
|
167
|
+
2. Execute each task.
|
|
168
|
+
3. **Always ack after handling**: `ufoo bus ack $UFOO_SUBSCRIBER_ID`
|
|
159
169
|
4. Reply only when substantive — send `ufoo bus send "<publisher>" "<result>"` only if:
|
|
160
170
|
- The sender asked a question → reply with the answer
|
|
161
171
|
- The sender delegated a task → reply with the result / artifact
|
|
@@ -164,6 +174,10 @@ Notes:
|
|
|
164
174
|
5. **Report** if the work arrived from `[manual]<to:...>` or `[ufoo]<from:...>`:
|
|
165
175
|
`ufoo report progress|done|error "<short summary>"`
|
|
166
176
|
|
|
177
|
+
If `/ubus` is explicitly requested, run
|
|
178
|
+
`ufoo bus check "$UFOO_SUBSCRIBER_ID"` and then follow the same execute → ack
|
|
179
|
+
flow.
|
|
180
|
+
|
|
167
181
|
---
|
|
168
182
|
|
|
169
183
|
## 4. Message Format
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "u-foo",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.12",
|
|
4
4
|
"description": "Multi-Agent Workspace Protocol. Just add u. claude → uclaude, codex → ucodex.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"homepage": "https://ufoo.dev",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Icyoung/ufoo.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/Icyoung/ufoo/issues"
|
|
13
|
+
},
|
|
7
14
|
"author": "UFOO Team",
|
|
8
15
|
"keywords": [
|
|
9
16
|
"cli",
|
|
@@ -29,6 +36,7 @@
|
|
|
29
36
|
"templates/",
|
|
30
37
|
"online/",
|
|
31
38
|
"scripts/",
|
|
39
|
+
"dist/tui/",
|
|
32
40
|
"SKILLS/",
|
|
33
41
|
"LICENSE",
|
|
34
42
|
"README.md"
|
|
@@ -39,6 +47,8 @@
|
|
|
39
47
|
},
|
|
40
48
|
"scripts": {
|
|
41
49
|
"postinstall": "node scripts/postinstall.js",
|
|
50
|
+
"pack:tui": "node scripts/pack-tui.js",
|
|
51
|
+
"prepack": "node scripts/pack-tui.js --check",
|
|
42
52
|
"test": "jest",
|
|
43
53
|
"test:watch": "jest --watch",
|
|
44
54
|
"test:coverage": "jest --coverage"
|
|
@@ -51,9 +61,7 @@
|
|
|
51
61
|
"chalk": "^4.1.2",
|
|
52
62
|
"commander": "^13.1.0",
|
|
53
63
|
"gray-matter": "^4.0.3",
|
|
54
|
-
"ink": "^5.2.1",
|
|
55
64
|
"node-pty": "^1.1.0",
|
|
56
|
-
"react": "^18.3.1",
|
|
57
65
|
"ws": "^8.19.0"
|
|
58
66
|
},
|
|
59
67
|
"devDependencies": {
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Stage a release `ufoo-tui` binary under dist/tui/<platform>-<arch>/.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node scripts/pack-tui.js # build current host + stage
|
|
10
|
+
* node scripts/pack-tui.js --copy-only # stage from existing target/release
|
|
11
|
+
* node scripts/pack-tui.js --check # exit 0 only if at least one staged binary exists
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require("fs");
|
|
15
|
+
const path = require("path");
|
|
16
|
+
const { spawnSync } = require("child_process");
|
|
17
|
+
|
|
18
|
+
const ROOT = path.resolve(__dirname, "..");
|
|
19
|
+
const DIST_ROOT = path.join(ROOT, "dist", "tui");
|
|
20
|
+
const BINARY_NAME = process.platform === "win32" ? "ufoo-tui.exe" : "ufoo-tui";
|
|
21
|
+
|
|
22
|
+
function hostPlatform() {
|
|
23
|
+
return `${process.platform}-${process.arch}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function listStagedPlatforms() {
|
|
27
|
+
if (!fs.existsSync(DIST_ROOT)) return [];
|
|
28
|
+
const out = [];
|
|
29
|
+
for (const entry of fs.readdirSync(DIST_ROOT, { withFileTypes: true })) {
|
|
30
|
+
if (!entry.isDirectory()) continue;
|
|
31
|
+
const bin = path.join(DIST_ROOT, entry.name, "ufoo-tui");
|
|
32
|
+
const binExe = path.join(DIST_ROOT, entry.name, "ufoo-tui.exe");
|
|
33
|
+
if (fs.existsSync(bin) || fs.existsSync(binExe)) out.push(entry.name);
|
|
34
|
+
}
|
|
35
|
+
return out.sort();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function findReleaseBinary() {
|
|
39
|
+
const candidates = [
|
|
40
|
+
path.join(ROOT, "target", "release", BINARY_NAME),
|
|
41
|
+
path.join(ROOT, "crates", "ufoo-tui", "target", "release", BINARY_NAME),
|
|
42
|
+
];
|
|
43
|
+
for (const candidate of candidates) {
|
|
44
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
45
|
+
return candidate;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function buildRelease() {
|
|
52
|
+
console.log("[pack-tui] cargo build -p ufoo-tui --release");
|
|
53
|
+
const result = spawnSync("cargo", ["build", "-p", "ufoo-tui", "--release"], {
|
|
54
|
+
cwd: ROOT,
|
|
55
|
+
stdio: "inherit",
|
|
56
|
+
env: process.env,
|
|
57
|
+
});
|
|
58
|
+
if (result.status !== 0) {
|
|
59
|
+
throw new Error("cargo build failed");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function stageBinary(sourcePath, platform = hostPlatform()) {
|
|
64
|
+
const destDir = path.join(DIST_ROOT, platform);
|
|
65
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
66
|
+
const destName = platform.startsWith("win32") ? "ufoo-tui.exe" : "ufoo-tui";
|
|
67
|
+
const destPath = path.join(destDir, destName);
|
|
68
|
+
fs.copyFileSync(sourcePath, destPath);
|
|
69
|
+
try {
|
|
70
|
+
fs.chmodSync(destPath, 0o755);
|
|
71
|
+
} catch {
|
|
72
|
+
// Windows may ignore chmod
|
|
73
|
+
}
|
|
74
|
+
console.log(`[pack-tui] staged ${destPath}`);
|
|
75
|
+
return destPath;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function main(argv = process.argv.slice(2)) {
|
|
79
|
+
const copyOnly = argv.includes("--copy-only");
|
|
80
|
+
const checkOnly = argv.includes("--check");
|
|
81
|
+
|
|
82
|
+
if (checkOnly) {
|
|
83
|
+
const plats = listStagedPlatforms();
|
|
84
|
+
if (plats.length === 0) {
|
|
85
|
+
console.error("[pack-tui] no staged binaries under dist/tui/<platform>/");
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
console.log(`[pack-tui] staged platforms: ${plats.join(", ")}`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!copyOnly) {
|
|
93
|
+
buildRelease();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const source = findReleaseBinary();
|
|
97
|
+
if (!source) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`release binary not found (looked for target/release/${BINARY_NAME}). `
|
|
100
|
+
+ "Run cargo build -p ufoo-tui --release first.",
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
stageBinary(source);
|
|
104
|
+
console.log(`[pack-tui] ok (${hostPlatform()})`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
main();
|
|
109
|
+
} catch (err) {
|
|
110
|
+
console.error(`[pack-tui] ${err && err.message ? err.message : err}`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
package/scripts/postinstall.js
CHANGED
|
@@ -30,6 +30,17 @@ for (const platform of platforms) {
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// Ensure the platform ufoo-tui binary from the npm pack is executable.
|
|
34
|
+
try {
|
|
35
|
+
const plat = `${process.platform}-${process.arch}`;
|
|
36
|
+
const tuiBin = path.join(__dirname, "..", "dist", "tui", plat, "ufoo-tui");
|
|
37
|
+
if (fs.existsSync(tuiBin)) {
|
|
38
|
+
fs.chmodSync(tuiBin, 0o755);
|
|
39
|
+
}
|
|
40
|
+
} catch {
|
|
41
|
+
// Non-fatal — doctor / launch will report missing binary.
|
|
42
|
+
}
|
|
43
|
+
|
|
33
44
|
// Collect all skill sources from the package-level SKILLS directory.
|
|
34
45
|
function collectSkillSources(pkgRoot) {
|
|
35
46
|
const sources = [];
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const { readJSON } = require("../../coordination/bus/utils");
|
|
5
|
+
|
|
6
|
+
const DEFAULT_GRACE_MS = 5000;
|
|
7
|
+
const BUSY_DISK_STATES = new Set(["working", "waiting_input", "blocked"]);
|
|
8
|
+
const DELIVERABLE_LOCAL_STATES = new Set(["idle", "ready"]);
|
|
9
|
+
|
|
10
|
+
function asState(value = "") {
|
|
11
|
+
return String(value || "").trim().toLowerCase();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function resolveGraceMs(detector, options = {}) {
|
|
15
|
+
if (Number.isFinite(options.graceMs) && options.graceMs > 0) {
|
|
16
|
+
return options.graceMs;
|
|
17
|
+
}
|
|
18
|
+
const quiet = Number(detector && detector.quietWindowMs);
|
|
19
|
+
const base = Number.isFinite(quiet) && quiet > 0 ? quiet : DEFAULT_GRACE_MS;
|
|
20
|
+
// Cap so internal-pty (30s quiet) does not leave inject stamps stuck that long.
|
|
21
|
+
return Math.min(base, DEFAULT_GRACE_MS);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readDiskMeta(agentsFile, subscriber) {
|
|
25
|
+
if (!agentsFile || !subscriber || !fs.existsSync(agentsFile)) return null;
|
|
26
|
+
try {
|
|
27
|
+
const data = readJSON(agentsFile, null);
|
|
28
|
+
const meta = data && data.agents && data.agents[subscriber];
|
|
29
|
+
return meta && typeof meta === "object" ? meta : null;
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Heal false `working` when the in-process ActivityDetector already believes
|
|
37
|
+
* the agent is deliverable (idle/ready) but all-agents.json still says busy.
|
|
38
|
+
*
|
|
39
|
+
* Typical cause: DeliveryScheduler stamps `working` (detail=inject) after
|
|
40
|
+
* inject while the detector never saw meaningful PTY output, so it never
|
|
41
|
+
* re-publishes idle (publisher dedupe + no state transition).
|
|
42
|
+
*
|
|
43
|
+
* Waits `graceMs` (≤ quiet window, capped at 5s) after disk `activity_since`
|
|
44
|
+
* so the inject stamp can still close the mid-turn double-inject race.
|
|
45
|
+
*
|
|
46
|
+
* Does not clear `waiting_input` / `blocked` — those need explicit recovery.
|
|
47
|
+
*
|
|
48
|
+
* @returns {{ ok: boolean, reconciled: boolean, reason: string }}
|
|
49
|
+
*/
|
|
50
|
+
function reconcileDetectorOwnedActivity(options = {}) {
|
|
51
|
+
const {
|
|
52
|
+
detector,
|
|
53
|
+
publisher,
|
|
54
|
+
agentsFile,
|
|
55
|
+
subscriber,
|
|
56
|
+
now = Date.now(),
|
|
57
|
+
} = options;
|
|
58
|
+
|
|
59
|
+
if (!detector || !publisher || !agentsFile || !subscriber) {
|
|
60
|
+
return { ok: false, reconciled: false, reason: "missing" };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const snap = typeof detector.getState === "function"
|
|
64
|
+
? detector.getState()
|
|
65
|
+
: { state: "", since: now, detail: "" };
|
|
66
|
+
const local = asState(snap.state);
|
|
67
|
+
if (!DELIVERABLE_LOCAL_STATES.has(local)) {
|
|
68
|
+
return { ok: true, reconciled: false, reason: "local_busy" };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const diskMeta = readDiskMeta(agentsFile, subscriber);
|
|
72
|
+
if (!diskMeta) {
|
|
73
|
+
return { ok: false, reconciled: false, reason: "missing_agent" };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const diskState = asState(diskMeta.activity_state);
|
|
77
|
+
if (diskState !== "working") {
|
|
78
|
+
return { ok: true, reconciled: false, reason: BUSY_DISK_STATES.has(diskState) ? "disk_gated" : "disk_ok" };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const graceMs = resolveGraceMs(detector, options);
|
|
82
|
+
const sinceMs = Date.parse(String(diskMeta.activity_since || ""));
|
|
83
|
+
const diskAge = Number.isFinite(sinceMs) ? Math.max(0, now - sinceMs) : graceMs;
|
|
84
|
+
if (diskAge < graceMs) {
|
|
85
|
+
return { ok: true, reconciled: false, reason: "grace" };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const publishState = local === "ready" ? "idle" : local;
|
|
89
|
+
const changed = publisher.publish(publishState, {
|
|
90
|
+
since: snap.since,
|
|
91
|
+
previous: diskState,
|
|
92
|
+
detail: "",
|
|
93
|
+
}, { force: true, reconcile: true });
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
ok: true,
|
|
97
|
+
reconciled: Boolean(changed),
|
|
98
|
+
reason: changed ? "cleared" : "unchanged",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = {
|
|
103
|
+
DEFAULT_GRACE_MS,
|
|
104
|
+
reconcileDetectorOwnedActivity,
|
|
105
|
+
resolveGraceMs,
|
|
106
|
+
};
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
1
3
|
const fs = require("fs");
|
|
2
4
|
const path = require("path");
|
|
5
|
+
const { readJSON } = require("../../coordination/bus/utils");
|
|
3
6
|
const { writeActivityState } = require("./activityStateWriter");
|
|
4
7
|
|
|
5
8
|
/**
|
|
@@ -16,7 +19,9 @@ const { writeActivityState } = require("./activityStateWriter");
|
|
|
16
19
|
* @param {string} options.subscriber - Subscriber ID (e.g. "claude-code:abc123")
|
|
17
20
|
* @param {string} options.projectRoot - Project root (unused, kept for API compat)
|
|
18
21
|
* @param {boolean} [options.force=true] - Force overwrite priority-protected states
|
|
19
|
-
* publish(state, extra, { force }) can override
|
|
22
|
+
* publish(state, extra, { force, reconcile }) can override defaults for one
|
|
23
|
+
* transition. `reconcile: true` skips in-memory dedupe when disk disagrees
|
|
24
|
+
* (daemon inject stamp / lost-update healing).
|
|
20
25
|
*/
|
|
21
26
|
function createActivityStatePublisher(options = {}) {
|
|
22
27
|
const {
|
|
@@ -28,9 +33,33 @@ function createActivityStatePublisher(options = {}) {
|
|
|
28
33
|
let lastState = "";
|
|
29
34
|
let lastDetail = "";
|
|
30
35
|
|
|
36
|
+
function readDiskActivity() {
|
|
37
|
+
try {
|
|
38
|
+
if (!agentsFile || !fs.existsSync(agentsFile)) return null;
|
|
39
|
+
const data = readJSON(agentsFile, null);
|
|
40
|
+
const meta = data && data.agents && data.agents[subscriber];
|
|
41
|
+
if (!meta || typeof meta !== "object") return null;
|
|
42
|
+
return {
|
|
43
|
+
state: typeof meta.activity_state === "string" ? meta.activity_state : "",
|
|
44
|
+
detail: typeof meta.activity_detail === "string" ? meta.activity_detail : "",
|
|
45
|
+
};
|
|
46
|
+
} catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
31
51
|
function publish(state, extra = {}, publishOptions = {}) {
|
|
32
52
|
const detail = typeof extra.detail === "string" ? extra.detail : "";
|
|
33
|
-
|
|
53
|
+
const reconcile = publishOptions.reconcile === true;
|
|
54
|
+
if (!reconcile && state === lastState && detail === lastDetail) return false;
|
|
55
|
+
if (reconcile) {
|
|
56
|
+
const disk = readDiskActivity();
|
|
57
|
+
if (disk && disk.state === state && disk.detail === detail) {
|
|
58
|
+
lastState = state;
|
|
59
|
+
lastDetail = detail;
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
34
63
|
const since = extra.since || undefined;
|
|
35
64
|
const effectiveForce = typeof publishOptions.force === "boolean"
|
|
36
65
|
? publishOptions.force
|
|
@@ -10,6 +10,7 @@ const { showBanner } = require("../../ui/format/banner");
|
|
|
10
10
|
const AgentNotifier = require("./notifier");
|
|
11
11
|
const { ActivityDetector } = require("../activity/activityDetector");
|
|
12
12
|
const { createActivityStatePublisher } = require("../activity/activityStatePublisher");
|
|
13
|
+
const { reconcileDetectorOwnedActivity } = require("../activity/activityReconcile");
|
|
13
14
|
const { detectLaunchEnvironment } = require("./launchEnvironment");
|
|
14
15
|
const { getUfooPaths } = require("../../coordination/state/paths");
|
|
15
16
|
const { createTerminalAdapterRouter } = require("../../runtime/terminal/adapterRouter");
|
|
@@ -697,6 +698,23 @@ class AgentLauncher {
|
|
|
697
698
|
detail: snap.detail,
|
|
698
699
|
});
|
|
699
700
|
});
|
|
701
|
+
// Heal daemon inject-stamp desync (disk working, detector idle).
|
|
702
|
+
const agentsFilePath = getUfooPaths(this.cwd).agentsFile;
|
|
703
|
+
const activityReconcileTimer = setInterval(() => {
|
|
704
|
+
try {
|
|
705
|
+
reconcileDetectorOwnedActivity({
|
|
706
|
+
detector: launcherActivityDetector,
|
|
707
|
+
publisher: launcherPublisher,
|
|
708
|
+
agentsFile: agentsFilePath,
|
|
709
|
+
subscriber: subscriberId,
|
|
710
|
+
});
|
|
711
|
+
} catch {
|
|
712
|
+
// reconcile must never break the launcher
|
|
713
|
+
}
|
|
714
|
+
}, 2000);
|
|
715
|
+
if (typeof activityReconcileTimer.unref === "function") {
|
|
716
|
+
activityReconcileTimer.unref();
|
|
717
|
+
}
|
|
700
718
|
// Tail buffer for agy: when the TUI exits, agy prints a single line
|
|
701
719
|
// `Resume: agy --conversation=<UUID> (or -c)` to stdout. We keep
|
|
702
720
|
// the trailing ~4KB of post-frame output around and grep it on exit
|
|
@@ -763,6 +781,7 @@ class AgentLauncher {
|
|
|
763
781
|
wrapper.onExit = async ({ exitCode, signal }) => {
|
|
764
782
|
// 清理 timers
|
|
765
783
|
clearTimeout(forceReadyTimer);
|
|
784
|
+
clearInterval(activityReconcileTimer);
|
|
766
785
|
launcherActivityDetector.destroy();
|
|
767
786
|
|
|
768
787
|
// Agy: harvest conversation id from the trailing `Resume: agy
|