pi-pier 0.1.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 +62 -0
- package/package.json +55 -0
- package/schemas/role-manifest.schema.json +53 -0
- package/skills/pi-herdr-workflow/SKILL.md +86 -0
- package/src/bootstrap.ts +106 -0
- package/src/core/grid-shape.ts +86 -0
- package/src/core/subagent.ts +1256 -0
- package/src/core/terminal.ts +359 -0
- package/src/core/todo.ts +313 -0
- package/src/gc-core.ts +56 -0
- package/src/herdr-client.ts +579 -0
- package/src/history-store.ts +115 -0
- package/src/index.ts +751 -0
- package/src/ledger.ts +71 -0
- package/src/lock-core.ts +155 -0
- package/src/manifest-compose.ts +102 -0
- package/src/notice-buffer.ts +28 -0
- package/src/pane-title.ts +84 -0
- package/src/pi-surface.ts +135 -0
- package/src/pipe-channel.ts +181 -0
- package/src/progress-core.ts +109 -0
- package/src/reconcile-core.ts +130 -0
- package/src/role-loader.ts +164 -0
- package/src/role-manifest.ts +166 -0
- package/src/roles/master.json +21 -0
- package/src/roles/worker-default.json +30 -0
- package/src/session-tail.ts +169 -0
- package/src/subagent-core.ts +310 -0
- package/src/subagent-scope.ts +39 -0
- package/src/terminal-core.ts +328 -0
- package/src/todo-core.ts +331 -0
- package/src/todo-read-hook.ts +54 -0
- package/src/todos-service.ts +66 -0
- package/src/tool-gate.ts +106 -0
- package/src/vocab.ts +82 -0
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# pi-pier
|
|
2
|
+
|
|
3
|
+
**pier** = **pi** × h**erdr** — a [pi](https://pi.dev/) extension that fuses coding agent sessions with the [herdr](https://herdr.dev/) pane/tab orchestrator.
|
|
4
|
+
|
|
5
|
+
`pi-pier` is the **pi extension half** of the [pier monorepo](https://github.com/July24/pier). It fills two gaps pi deliberately leaves open — the **todo list loop** and **interactive subagents** — using herdr panes/tabs as the visual and interaction substrate. It degrades gracefully outside herdr.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pi install npm:pi-pier
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or from git:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
pi install git:github.com/July24/pier
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
> The companion **herdr plugin half** (`pier.workbench`: main-tab bootstrap, blocked-gate notifications, focus-heat layout) is installed separately via `herdr plugin install July24/pier/packages/pier-workbench`, or all at once with the one-shot installer in the [repo](https://github.com/July24/pier): `node install.mjs`.
|
|
20
|
+
|
|
21
|
+
## What you get
|
|
22
|
+
|
|
23
|
+
### Tools
|
|
24
|
+
|
|
25
|
+
| Tool | Purpose |
|
|
26
|
+
|---|---|
|
|
27
|
+
| `todo_write` | Full-replacement todo list. Session JSONL is the single source of truth; correct rollback on branch switches. Projected live onto the pane title (`▶i ○p ■b ✓c (N/M) · current task`). `/todos` command to view/edit/unblock |
|
|
28
|
+
| `subagent` | Delegate self-contained subtasks to an isolated pi session in its own herdr pane (separate context window). Foreground / parallel / background modes |
|
|
29
|
+
| `list_agents` | Live state of background subagents (running / idle + panes) |
|
|
30
|
+
| `send_message` | Follow-up work for a subagent (queued if busy, wakes if idle) |
|
|
31
|
+
| `interrupt_agent` | Stop a subagent's current turn; it stays alive for more messages |
|
|
32
|
+
|
|
33
|
+
### Behaviors
|
|
34
|
+
|
|
35
|
+
- **Human-in-the-loop**: every subagent is a visible, interactive TUI pane — step into it anytime to talk directly (fix bugs, take over, answer its `ask_user_question`). Blocked gates raise sidebar markers + notifications
|
|
36
|
+
- **Soft locks**: write paths lock per-pane; conflicts warn/block instead of racing
|
|
37
|
+
- **Role profiles**: built-in `master` / `worker-default`; custom roles mount from `.pi-herdr/roles/<name>.json`. Toolsets converge per role — deny rules cannot be bypassed
|
|
38
|
+
- **Settlement notices folded**: background subagent completions inject between turns (max 3 shown, rest collapsed) instead of flood-filling at run end
|
|
39
|
+
|
|
40
|
+
## Scope & degradation
|
|
41
|
+
|
|
42
|
+
Outside a herdr environment the extension auto-degrades: herdr reporting costs nothing, `subagent` / terminal tools return "requires a herdr-managed pane" hints, while `todo_write` / `ask_user_question` keep working (session JSONL as authority, no herdr dependency).
|
|
43
|
+
|
|
44
|
+
## Requirements
|
|
45
|
+
|
|
46
|
+
- Node ≥ 22
|
|
47
|
+
- pi ≥ 0.84 (`@earendil-works/pi-coding-agent`)
|
|
48
|
+
- herdr ≥ 0.8.0 for the full pane experience (optional)
|
|
49
|
+
|
|
50
|
+
## Development
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
git clone https://github.com/July24/pier && cd pier
|
|
54
|
+
npm install --ignore-scripts
|
|
55
|
+
npm test # node --test, ~250 unit tests
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
See the [monorepo README](https://github.com/July24/pier) for design principles, the herdr-plugin half, and the one-shot installer.
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
|
|
62
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-pier",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "pier: pi extension that fuses pi agent sessions with the herdr pane/tab orchestrator — todo_write loop closure + interactive subagents in isolated panes",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"pi-package",
|
|
9
|
+
"pi",
|
|
10
|
+
"pi-extension",
|
|
11
|
+
"herdr",
|
|
12
|
+
"coding-agent",
|
|
13
|
+
"subagents",
|
|
14
|
+
"todo"
|
|
15
|
+
],
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/July24/pier.git",
|
|
19
|
+
"directory": "packages/pier-ext"
|
|
20
|
+
},
|
|
21
|
+
"homepage": "https://github.com/July24/pier#readme",
|
|
22
|
+
"bugs": {
|
|
23
|
+
"url": "https://github.com/July24/pier/issues"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=22"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"src",
|
|
30
|
+
"skills",
|
|
31
|
+
"schemas",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"pi": {
|
|
35
|
+
"extensions": [
|
|
36
|
+
"./src/index.ts"
|
|
37
|
+
],
|
|
38
|
+
"skills": [
|
|
39
|
+
"./skills/pi-herdr-workflow"
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@deepseek-ai/cordis": "4.0.1",
|
|
44
|
+
"@deepseek-ai/cordis-plugin-group": "^1.0.1",
|
|
45
|
+
"@deepseek-ai/cordis-plugin-loader": "^1.0.2"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
49
|
+
"typebox": "*"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@earendil-works/pi-coding-agent": "0.84.2",
|
|
53
|
+
"typebox": "~1.3.7"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://herdr-pi.local/schemas/role-manifest.schema.json",
|
|
4
|
+
"title": "herdr-pi role manifest(D76 v2)",
|
|
5
|
+
"description": "档2 role 档案契约:version(P2 语义版本)+ roleType(P3 planner/executor/hybrid)+ 三态 rules(P1 allow/ask/deny)。运行时校验为 src/role-manifest.ts 手写零依赖实现,本文件是合同文档。",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"role": { "type": "string", "pattern": "^[a-z0-9-]+$", "description": "角色名,须与文件名一致" },
|
|
9
|
+
"version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+$", "description": "语义版本 x.y.z" },
|
|
10
|
+
"model": { "type": "string", "pattern": "^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$", "description": "按角色路由模型 provider/model(WS-D10;可省略 = 进程默认)" },
|
|
11
|
+
"description": { "type": "string" },
|
|
12
|
+
"manifest": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"properties": {
|
|
15
|
+
"tools": {
|
|
16
|
+
"type": "array",
|
|
17
|
+
"minItems": 1,
|
|
18
|
+
"items": { "type": "string", "pattern": "^[a-z0-9_-]+$" },
|
|
19
|
+
"uniqueItems": true,
|
|
20
|
+
"description": "基线工具;必含 todo_write 与 ask_user_question(协调工具,运行时校验强制)"
|
|
21
|
+
},
|
|
22
|
+
"rules": {
|
|
23
|
+
"type": "object",
|
|
24
|
+
"propertyNames": { "pattern": "^[a-z0-9_*-]+$" },
|
|
25
|
+
"additionalProperties": { "enum": ["allow", "ask", "deny"] },
|
|
26
|
+
"description": "三态权限;* 为通配符默认;可省略 = {\"*\":\"allow\"}"
|
|
27
|
+
},
|
|
28
|
+
"unknownTools": {
|
|
29
|
+
"enum": ["allow", "deny"],
|
|
30
|
+
"description": "D82 未知工具姿态(缺省 deny):allow = 用户安装的扩展默认可见可用(master/worker);deny = 清单外一律不可见。与 rules 的 * 通配分属两轴(可见性 vs 强制力)"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"required": ["tools"],
|
|
34
|
+
"additionalProperties": false
|
|
35
|
+
},
|
|
36
|
+
"services": {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"properties": {
|
|
39
|
+
"todos": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"properties": {
|
|
42
|
+
"mode": { "enum": ["serial", "parallel"] },
|
|
43
|
+
"reminderLimit": { "type": "integer", "exclusiveMinimum": 0 }
|
|
44
|
+
},
|
|
45
|
+
"additionalProperties": false
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"additionalProperties": false
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"required": ["role", "version", "manifest"],
|
|
52
|
+
"additionalProperties": false
|
|
53
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pi-herdr-workflow
|
|
3
|
+
description: >-
|
|
4
|
+
How to use the pi-herdr workbench fusion: todo_write discipline, subagent
|
|
5
|
+
delegation (foreground/parallel/background), list_agents/send_message/
|
|
6
|
+
interrupt_agent, and how the human watches everything in herdr panes.
|
|
7
|
+
Load when the user asks about multi-step plans, delegating subtasks,
|
|
8
|
+
background work, or checking on running subagents.
|
|
9
|
+
license: MIT
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# pi-herdr 工作台工作流
|
|
13
|
+
|
|
14
|
+
你在一个 herdr 终端工作区(pane)里运行。本 skill 教你把 `todo_write` 与
|
|
15
|
+
`subagent` 工具用好;人类在 herdr 的 TUI 里实时观察:每个 pane 的窗格头
|
|
16
|
+
显示 `▶x ○y ■b ✓z · 当前任务`,语义状态(working/blocked/idle)上卷到侧边栏。
|
|
17
|
+
|
|
18
|
+
## 1. todo 纪律(全量替换语义)
|
|
19
|
+
|
|
20
|
+
- `todo_write` 每次调用提交**完整列表**(last-wins),没有部分更新。
|
|
21
|
+
- 条目 = `content`(非空、唯一、5–10 词的命令式短语,写"做什么"不写"怎么做",**不带编号前缀**)+ `status`(pending | in_progress | completed | blocked | abandoned)。
|
|
22
|
+
- `blocked` = 在等外部条件(信息、人、别的系统),必须带 `blocker` 说明等什么;`abandoned` = 显式放弃(不删条目,保留计划痕迹)。
|
|
23
|
+
- 多阶段计划用可选 `phase` 分组(≤30 字);单阶段就不写 phase。
|
|
24
|
+
- 保持列表精简(≤15 条):一条 = 一个有意义的交付单元,不是微步骤。
|
|
25
|
+
- 正在推进的标 `in_progress`(真正并行时才允许多条);**完成立即标 completed**。
|
|
26
|
+
- **测试失败/实现不完整/有未解决错误时绝不算 completed**——保持 in_progress,或改成 blocked 并说明 blocker。
|
|
27
|
+
- **绝不把 todo_write 当成一轮里的唯一工具调用**——搭着真实工作一起提交。
|
|
28
|
+
- 记不清某条任务的原文时,从上一轮 todo_write 结果里找,**不许凭记忆编**(防幻影条目)。
|
|
29
|
+
- **不要**把已委派给子代理的工作写进自己的 todo——那是子代理自己的职责;
|
|
30
|
+
你的列表只跟踪你自己的工作。等子代理结果回流后再验收归档。
|
|
31
|
+
- 多子代理协作时在 content 里写清**文件所有权**(例如 "重构 auth 模块(只碰 src/auth/)"),
|
|
32
|
+
避免两个 agent 抢同一文件。
|
|
33
|
+
- **你是被委派的工作 pane**(`PI_HERDR_SUBAGENT=1`)时,本插件自动执行严格模式:
|
|
34
|
+
你的列表任何时候**至多一条 in_progress**(多写会被归一化),全部完成会自动晋升下一条 pending。
|
|
35
|
+
你是主控 pane 时保持并行语义,纪律由本节承担。
|
|
36
|
+
|
|
37
|
+
## 2. 子代理委派
|
|
38
|
+
|
|
39
|
+
子代理 pane 跑的是**交互式 pi TUI**(独立会话):人类可以随时进入该 pane
|
|
40
|
+
直接与子代理对话;你的委派 prompt 会以消息形式出现在它的对话里。
|
|
41
|
+
|
|
42
|
+
### 选型
|
|
43
|
+
- **前台**(默认):调用后等待结果返回。适合"下一步依赖其结果"的委派。
|
|
44
|
+
- **后台**(`run_in_background: true`):立即返回 `agentId`;子代理在独立
|
|
45
|
+
pane 常驻运行。适合长任务/并行推进。它会**持续存在**(有自己的持久会话),
|
|
46
|
+
结算时你会收到一条 "Background subagent <id> finished..." 通知消息。
|
|
47
|
+
- 一条消息里的多个 subagent 调用会并行执行(上限 4 并发)。
|
|
48
|
+
- 可选 `role`(如 `advisor`)只是标签:v1 不改工具面、不改生命周期,与普通 task 一样走 TTL;省略 = `task`。复活走 `resume_subagent`,不要假设 pane 会自动补回来。
|
|
49
|
+
|
|
50
|
+
### 任务 tab 分组(`tab` 参数)
|
|
51
|
+
- 每个委派默认开一个**以任务命名的新 tab**(名字取自 `description`)。
|
|
52
|
+
- 一组相关的子任务用同一个 `tab` 名字收进同一个 tab(例如网络调研 + 现状
|
|
53
|
+
摸索都用 `tab: "调研"`),人类在 herdr 里一个 tab 内看完全部相关 pane。
|
|
54
|
+
- 名字要短(≤20 字)、人类一眼能懂;tab 栏就是任务板。
|
|
55
|
+
- 不要为了少开 tab 而把无关任务塞进同一个 tab;一个 tab = 一个任务目标。
|
|
56
|
+
- **任务 tab 是临时的**:tab 内全部子代理结算后(默认宽限约 10 分钟)tab
|
|
57
|
+
会自动关闭——不要假设它的 pane 长期存在;需要回看/继续时用
|
|
58
|
+
`resume_subagent(taskId)` 从历史复活,而不是引用已关的 pane。
|
|
59
|
+
|
|
60
|
+
### prompt 纪律
|
|
61
|
+
- prompt 必须**自包含**:子代理看不到你的对话历史,只有这段 prompt。
|
|
62
|
+
- 明确输入(哪些文件/事实)、明确产出(交什么)、明确边界(不碰什么)。
|
|
63
|
+
- 用文件/工件传递共享上下文,不要把大段内容塞进 prompt。
|
|
64
|
+
- 独立任务才委派;互相依赖的工作拆给同一子代理。
|
|
65
|
+
|
|
66
|
+
### 后台子代理的后续管理
|
|
67
|
+
- `list_agents`:查看全部后台子代理与状态(running/idle)。
|
|
68
|
+
- `send_message(agentId, message)`:给子代理追加任务(FIFO;已结算的子代理
|
|
69
|
+
会被唤醒继续干)。
|
|
70
|
+
- `interrupt_agent(agentId)`:打断子代理当前轮次(它仍驻留,可继续 send_message)。
|
|
71
|
+
- 子代理结算后不要重新委派同一个 agentId 之外的新任务时,可再 send_message 唤醒。
|
|
72
|
+
|
|
73
|
+
## 3. 人类闸门
|
|
74
|
+
|
|
75
|
+
- 需要人类决策(审批、取舍、方向选择)时,用 `ask_user_question`(本插件提供)——
|
|
76
|
+
等待回答期间,你的 pane 在 herdr 里呈 **blocked** 状态,人类会看到并介入;
|
|
77
|
+
答案会作为工具结果回到你的上下文,然后继续工作。
|
|
78
|
+
- 子代理 pane 在 herdr 里可被人类直接进入交互;你的委派描述(pane 标签)
|
|
79
|
+
要写得人类一眼能懂。
|
|
80
|
+
|
|
81
|
+
## 4. 多 agent 纪律(工程实践映射)
|
|
82
|
+
|
|
83
|
+
- 你是仲裁者:验收每个子代理的输出,再合入你的工作;不要把仲裁权下放。
|
|
84
|
+
- 关键结论安排独立复核(换个子代理 reviewer 复核),不盲信单一子代理。
|
|
85
|
+
- 同构的并行子代理会犯同样的错:给不同子代理不同的任务边界与产出格式。
|
|
86
|
+
- 委派后继续做你自己的事,不要干等(前台调用除外)。
|
package/src/bootstrap.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 档1 bootstrap:master 进程的 cordis 树根(D78 挂载树 / D80 生态采用 / D81 三分法)。
|
|
3
|
+
*
|
|
4
|
+
* 形态:Loader 作根插件 → builtins.group 注册(D80③ `cordis:group`)→
|
|
5
|
+
* timer(hmr 前置依赖,D80②)→ hmr(仅开发姿态:`--expose-internals` 且
|
|
6
|
+
* env PI_HERDR_HMR=1,缺一即跳过——生产路径零 watcher)。
|
|
7
|
+
*
|
|
8
|
+
* worker 进程不走本模块(C3/D81:短命进程手动 mount,无 loader/hmr)——
|
|
9
|
+
* 与 subagent-scope.ts 同为 master 分支动态 import。
|
|
10
|
+
*
|
|
11
|
+
* 后续:core 模块(todo/subagent/terminal/approval/reconcile 族)逐个迁为
|
|
12
|
+
* loader entry(热换面 = entry,D80③ 手动 mount 不热换)。
|
|
13
|
+
*/
|
|
14
|
+
import { Context } from '@deepseek-ai/cordis';
|
|
15
|
+
import Loader from '@deepseek-ai/cordis-plugin-loader';
|
|
16
|
+
import Group from '@deepseek-ai/cordis-plugin-group';
|
|
17
|
+
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
18
|
+
import * as path from 'node:path';
|
|
19
|
+
import { DisposeLedger } from './ledger.ts';
|
|
20
|
+
|
|
21
|
+
export interface BootstrapHooks {
|
|
22
|
+
onDispose?: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CordisApp {
|
|
26
|
+
/** 树根(subagent scope 与 lifecycle effect 都挂这里)。 */
|
|
27
|
+
root: Context;
|
|
28
|
+
/** loader service 是否就绪(false = 降级为裸树,行为同档0)。 */
|
|
29
|
+
loaderReady: boolean;
|
|
30
|
+
/** hmr 是否激活(开发姿态才有)。 */
|
|
31
|
+
hmrActive: boolean;
|
|
32
|
+
/** dispose 账本(D80⑤ hmr 补偿 + D79 反注册共用;session_shutdown 走 disposeAll)。 */
|
|
33
|
+
ledger: DisposeLedger;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** node 是否带 --expose-internals(hmr 的硬前提,D80①)。 */
|
|
37
|
+
export function detectExposeInternals(): boolean {
|
|
38
|
+
return process.execArgv.some((a) => a === '--expose-internals' || a.startsWith('--expose-internals='));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function createCordisApp(hooks: BootstrapHooks = {}): Promise<CordisApp> {
|
|
42
|
+
const root = new Context();
|
|
43
|
+
const ledger = new DisposeLedger();
|
|
44
|
+
// 服务注入(cordis 原生 DI):core entry 用 ctx.get('pi-herdr.ledger') 取账本——
|
|
45
|
+
// (entry config 过不了活实例,loader 会变换 config;provide/get 是 hmr/timer 同款机制)
|
|
46
|
+
root.provide('pi-herdr.ledger', ledger);
|
|
47
|
+
if (hooks.onDispose) {
|
|
48
|
+
root.effect(() => () => { hooks.onDispose?.(); }, 'session-root');
|
|
49
|
+
}
|
|
50
|
+
// 相对名 entry(./core/...)以本文件目录解析
|
|
51
|
+
root.baseUrl = pathToFileURL(path.dirname(fileURLToPath(import.meta.url))).href + '/';
|
|
52
|
+
|
|
53
|
+
let loaderReady = false;
|
|
54
|
+
let hmrActive = false;
|
|
55
|
+
try {
|
|
56
|
+
await root.plugin(Loader);
|
|
57
|
+
const withLoader = root as Context & { loader?: { builtins: Record<string, unknown> } };
|
|
58
|
+
if (withLoader.loader) {
|
|
59
|
+
withLoader.loader.builtins.group = Group; // D80③:声明里用 cordis:group 引用
|
|
60
|
+
loaderReady = true;
|
|
61
|
+
}
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.error(`[pi-herdr] cordis loader degraded (bare tree): ${err instanceof Error ? err.message : String(err)}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (loaderReady && detectExposeInternals() && process.env.PI_HERDR_HMR === '1') {
|
|
67
|
+
try {
|
|
68
|
+
const withLoader = root as Context & {
|
|
69
|
+
loader?: { create: (o: { name: string; config?: unknown }) => Promise<unknown> };
|
|
70
|
+
};
|
|
71
|
+
await withLoader.loader?.create({ name: '@deepseek-ai/cordis-plugin-timer' });
|
|
72
|
+
await withLoader.loader?.create({
|
|
73
|
+
name: '@deepseek-ai/cordis-plugin-hmr',
|
|
74
|
+
config: {
|
|
75
|
+
root: [path.dirname(fileURLToPath(import.meta.url))],
|
|
76
|
+
debounce: 200,
|
|
77
|
+
ignored: [],
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
hmrActive = true;
|
|
81
|
+
console.error('[pi-herdr] hmr active (dev posture: --expose-internals + PI_HERDR_HMR=1)');
|
|
82
|
+
// D80⑤(修正后):hmr reload **会**拆旧 fiber 的 effect-disposers(registry.delete)——
|
|
83
|
+
// ctx.effect 是 core entry 主拆除机制。本 hook 补的是账本侧:pi-surface 旧世代
|
|
84
|
+
// 收割(d87 世代化:重挂后的 disposeKey 只杀登记世代及之前,新世代豁免)。
|
|
85
|
+
root.on('hmr/reload', (reloads: unknown) => {
|
|
86
|
+
try {
|
|
87
|
+
const files: string[] = [];
|
|
88
|
+
for (const r of (reloads as Map<unknown, { filename?: string }>).values()) {
|
|
89
|
+
if (typeof r?.filename === 'string') files.push(r.filename);
|
|
90
|
+
}
|
|
91
|
+
if (files.length > 0) {
|
|
92
|
+
const n = ledger.disposeKey(files);
|
|
93
|
+
if (n > 0) console.error(`[pi-herdr] ledger: compensated ${n} disposal(s) for ${files.join(', ')}`);
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
/* 补偿尽力而为 */
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
} catch (err) {
|
|
100
|
+
// D80②:hmr 挂失败不致命——降级为零 watcher,树其余部分照常
|
|
101
|
+
console.error(`[pi-herdr] hmr mount failed (degraded): ${err instanceof Error ? err.message : String(err)}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { root, loaderReady, hmrActive, ledger };
|
|
106
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D91 网格形态:spawn 时挑「最大格子沿长轴分裂」,让 pane 自然铺成方格
|
|
3
|
+
* (用户愿景:股票热力图式方格;横向格更易读最新输出)。
|
|
4
|
+
*
|
|
5
|
+
* 依据 herdr 0.8.2 源码:ratio = split 节点 first child 份额(split_rect:
|
|
6
|
+
* first_w = width*ratio);layout.apply 不能挂接既有 pane(只建新 shell),
|
|
7
|
+
* 因此网格形态只能在 spawn 增量分裂时养成,形态之外的尺寸归热力 reflow 管。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type ShapeNode =
|
|
11
|
+
| { type: 'pane'; paneId: string }
|
|
12
|
+
| { type: 'split'; direction: string; ratio: number; first: ShapeNode; second: ShapeNode };
|
|
13
|
+
|
|
14
|
+
/** 容错解析 layout.export 的 root(pane 叶:{type:'pane', pane:{pane_id}})。 */
|
|
15
|
+
export function parseShapeTree(raw: unknown): ShapeNode | null {
|
|
16
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
17
|
+
const o = raw as Record<string, unknown>;
|
|
18
|
+
if (o.type === 'pane') {
|
|
19
|
+
const nested = o.pane as Record<string, unknown> | undefined;
|
|
20
|
+
const id = typeof o.pane_id === 'string'
|
|
21
|
+
? o.pane_id
|
|
22
|
+
: typeof nested?.pane_id === 'string' ? nested.pane_id : null;
|
|
23
|
+
return id ? { type: 'pane', paneId: id } : null;
|
|
24
|
+
}
|
|
25
|
+
if (o.type === 'split') {
|
|
26
|
+
const first = parseShapeTree(o.first);
|
|
27
|
+
const second = parseShapeTree(o.second);
|
|
28
|
+
if (!first || !second) return null;
|
|
29
|
+
return {
|
|
30
|
+
type: 'split',
|
|
31
|
+
direction: String(o.direction ?? 'right'),
|
|
32
|
+
ratio: typeof o.ratio === 'number' ? o.ratio : 0.5,
|
|
33
|
+
first,
|
|
34
|
+
second,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface PaneCell {
|
|
41
|
+
id: string;
|
|
42
|
+
x: number;
|
|
43
|
+
y: number;
|
|
44
|
+
w: number;
|
|
45
|
+
h: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 先序计算每个 pane 的格子(终端字符坐标;宽高仅用于比较,取典型 TUI 比例 200×50)。 */
|
|
49
|
+
export function paneCells(root: ShapeNode, width = 200, height = 50): PaneCell[] {
|
|
50
|
+
const cells: PaneCell[] = [];
|
|
51
|
+
const walk = (node: ShapeNode, x: number, y: number, w: number, h: number): void => {
|
|
52
|
+
if (node.type === 'pane') {
|
|
53
|
+
cells.push({ id: node.paneId, x, y, w, h });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const horizontal = node.direction === 'right' || node.direction === 'left' || node.direction === 'horizontal';
|
|
57
|
+
if (horizontal) {
|
|
58
|
+
walk(node.first, x, y, w * node.ratio, h);
|
|
59
|
+
walk(node.second, x + w * node.ratio, y, w * (1 - node.ratio), h);
|
|
60
|
+
} else {
|
|
61
|
+
walk(node.first, x, y, w, h * node.ratio);
|
|
62
|
+
walk(node.second, x, y + h * node.ratio, w, h * (1 - node.ratio));
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
walk(root, 0, 0, width, height);
|
|
66
|
+
return cells;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 挑分裂目标:面积最大的格子(并列取先序第一,稳定),沿长轴分裂——
|
|
71
|
+
* 宽 ≥ 1.3×高 → right(左右分,保格子横向可读),否则 down。
|
|
72
|
+
* exclude = 不许动的格子(board 等无 agent 常驻 shell)。
|
|
73
|
+
*/
|
|
74
|
+
export function pickGridSplit(
|
|
75
|
+
root: ShapeNode,
|
|
76
|
+
opts: { exclude?: ReadonlySet<string>; width?: number; height?: number } = {},
|
|
77
|
+
): { targetPaneId: string; direction: 'right' | 'down' } | null {
|
|
78
|
+
const cells = paneCells(root, opts.width ?? 200, opts.height ?? 50)
|
|
79
|
+
.filter((c) => !opts.exclude?.has(c.id));
|
|
80
|
+
if (cells.length === 0) return null;
|
|
81
|
+
let best = cells[0];
|
|
82
|
+
for (const c of cells) {
|
|
83
|
+
if (c.w * c.h > best.w * best.h + 1e-9) best = c;
|
|
84
|
+
}
|
|
85
|
+
return { targetPaneId: best.id, direction: best.w > best.h * 1.3 ? 'right' : 'down' };
|
|
86
|
+
}
|