@zhushanwen/pi-subagent-workflow 8.7.0 → 8.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.
Files changed (49) hide show
  1. package/README.md +5 -11
  2. package/package.json +7 -10
  3. package/skills/workflow-script-format/SKILL.md +32 -13
  4. package/src/host/__tests__/pi-host.test.ts +23 -2
  5. package/src/host/pi-host.ts +57 -3
  6. package/src/index.ts +56 -110
  7. package/src/injectors/__tests__/engine-awareness.test.ts +2 -2
  8. package/src/injectors/__tests__/engine-section-stability.test.ts +6 -4
  9. package/src/injectors/__tests__/model-list-injector.test.ts +18 -21
  10. package/src/injectors/__tests__/subagent-list-injector.test.ts +54 -14
  11. package/src/injectors/__tests__/workflow-list-injector.test.ts +26 -12
  12. package/src/injectors/engine-awareness.ts +0 -4
  13. package/src/injectors/model-list-injector.ts +15 -58
  14. package/src/injectors/subagent-list-injector.ts +55 -113
  15. package/src/injectors/workflow-list-injector.ts +33 -66
  16. package/src/interface/__tests__/detectors.test.ts +45 -34
  17. package/src/interface/__tests__/subagent-tool-prompt.test.ts +6 -3
  18. package/src/interface/__tests__/tool-workflow-run-builtin-name.test.ts +216 -0
  19. package/src/interface/__tests__/tool-workflow-script-generate.test.ts +17 -6
  20. package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +37 -1
  21. package/src/interface/bg-notify-render.ts +3 -13
  22. package/src/interface/command-actions.ts +5 -15
  23. package/src/interface/commands.ts +1 -1
  24. package/src/interface/format.ts +15 -4
  25. package/src/interface/gui-mappers.ts +18 -22
  26. package/src/interface/helpers.ts +8 -129
  27. package/src/interface/list-component.ts +1 -1
  28. package/src/interface/list-shared.ts +1 -1
  29. package/src/interface/list-view.ts +1 -1
  30. package/src/interface/subagent-actions.ts +51 -676
  31. package/src/interface/subagent-tool-schema.ts +1 -4
  32. package/src/interface/subagent-tool.ts +1 -1
  33. package/src/interface/subagents.ts +1 -1
  34. package/src/interface/tool-render.ts +3 -13
  35. package/src/interface/tool-workflow-script.ts +33 -75
  36. package/src/interface/tool-workflow.ts +51 -88
  37. package/src/interface/views/WorkflowsView.ts +3 -11
  38. package/src/interface/views/format.ts +19 -58
  39. package/src/jsonl-run-store.ts +134 -222
  40. package/agents/analyst.md +0 -61
  41. package/agents/coder.md +0 -70
  42. package/agents/debugger.md +0 -67
  43. package/agents/doc-reviewer.md +0 -50
  44. package/agents/explorer.md +0 -64
  45. package/agents/general-purpose.md +0 -32
  46. package/agents/orchestrator.md +0 -63
  47. package/agents/planner.md +0 -54
  48. package/agents/researcher.md +0 -65
  49. package/agents/reviewer.md +0 -74
package/README.md CHANGED
@@ -4,7 +4,7 @@ Pi 的 subagent + workflow 合并包:任务委派 + 多 agent 编排(chain /
4
4
 
5
5
  ## 内置 Agents
6
6
 
7
- 按「读/写 × 视角」正交切分,9 个角色零重叠:
7
+ 按「读/写 × 视角」正交切分,10 个角色零重叠(C1 起随 `@zhushanwen/subagent-core` 的 `agents/` 资产分发,`<available_subagents>` 的 `<location>` 指向 core 包目录):
8
8
 
9
9
  | Agent | 角色 | 读/写 |
10
10
  |-------|------|-------|
@@ -12,6 +12,7 @@ Pi 的 subagent + workflow 合并包:任务委派 + 多 agent 编排(chain /
12
12
  | `planner` | 复杂任务拆解为有序实施计划(合并需求澄清) | 只读产文档 |
13
13
  | `coder` | 代码实现、修改、测试(唯一改代码的角色) | 可改 |
14
14
  | `reviewer` | 代码审查与需求验收(含 git diff) | 只读 |
15
+ | `doc-reviewer` | 文档审查(四遍方法论,事实锚点核实;spec / 设计文档) | 只读 |
15
16
  | `debugger` | 运行时故障诊断,钉根因 | 只读* |
16
17
  | `analyst` | 深度项目分析,产出给人读的报告 | 只读 |
17
18
  | `researcher` | 外部资料调研(依赖 tavily skill) | 只读 |
@@ -30,19 +31,12 @@ Pi 的 subagent + workflow 合并包:任务委派 + 多 agent 编排(chain /
30
31
 
31
32
  ## Orchestrator 协调器模式
32
33
 
33
- agent 禁用 bash / read / write / edit 等执行工具,只保留协调类工具,被迫作为纯协调器:拆解任务 → 委派 subagent → 汇总结果。orchestrator agent 自身也可递归委派子 orchestrator,实现分层任务拆解(深度受 `Depth: N/10` 护栏保护)。
34
+ orchestrator 是纯协调器角色:拆解任务 → 委派 subagent → 汇总结果,自身不做执行类工作。orchestrator agent 自身也可递归委派子 orchestrator,实现分层任务拆解(深度受 `Depth: N/10` 护栏保护)。
34
35
 
35
- 可用工具 5 个:`todo`、`goal_control`、`workflow`、`subagent`、`ask_user`(`ask_user` `@zhushanwen/pi-ask-user` 提供,未安装时 orchestrator 遇歧义会明示停止而非猜测)。
36
-
37
- ### 启动命令
36
+ **工具约束变化(C1/D-5)**:内置模板不再携带 `tools:` frontmatter 白名单,subagent 不再以 `--tools` 白名单启动——工具约束回归宿主默认工具面,orchestrator 靠角色职责(职责边界段)约束自身只做协调。想要白名单的用户在 `<workspace>/.agents/agents/` 放同名 `.md` 覆写(project 级源稳定遮蔽内置,是唯一逃生门),或沿用 pi CLI 的 `--tools` 白名单(临时验证):
38
37
 
39
38
  ```bash
40
- # 方式一:CLI 工具白名单(临时验证最快)
41
39
  pi --tools todo,goal_control,workflow,subagent,ask_user
42
-
43
- # 方式二:白名单 + 注入 orchestrator 的 system prompt(推荐,主进程也具备协调器视角)
44
- pi --tools todo,goal_control,workflow,subagent,ask_user \
45
- --append-system-prompt "$(cat ~/.pi/agent/npm/node_modules/@zhushanwen/pi-subagent-workflow/agents/orchestrator.md)"
46
40
  ```
47
41
 
48
42
  > **依赖**:需先安装本包及相关扩展
@@ -59,7 +53,7 @@ pi --tools todo,goal_control,workflow,subagent,ask_user \
59
53
 
60
54
  ## Workflow 生命周期(one-shot)
61
55
 
62
- Workflow run 是一次性执行,状态机两态:`running → done`(`done` 唯一终态,reason 区分 completed / aborted / failed / budget_limited / time_limited)。`workflow` tool 仅 3 个 action:`run` / `status` / `abort`。
56
+ Workflow run 是一次性执行,状态机两态:`running → done`(`done` 唯一终态,reason 区分 completed / aborted / failed / budget_limited / time_limited)。`workflow` tool 仅 3 个 action:`run` / `status` / `abort`。run 的 `name` 接受 `<available_workflows>` 列出的 workflow 名(内置 chain / parallel / map-reduce / scatter-gather / review-fix-loop 或已保存脚本)或 `.js` 绝对路径。
63
57
 
64
58
  Runs are one-shot: there is no pause/resume — to stop a run early use abort; for a fresh result start a new run.
65
59
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-subagent-workflow",
3
- "version": "8.7.0",
3
+ "version": "8.8.0",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "description": "Unified subagent execution and multi-agent workflow orchestration for Pi — spawned-process agent runtime with sync/background modes, stateful workflow management with persistence, state machine, and execution tracing.",
@@ -19,7 +19,6 @@
19
19
  },
20
20
  "files": [
21
21
  "index.ts",
22
- "agents/",
23
22
  "skills/",
24
23
  "scripts/",
25
24
  "relay/",
@@ -51,24 +50,22 @@
51
50
  ],
52
51
  "skills": [
53
52
  "./skills"
54
- ],
55
- "agents": [
56
- "./agents"
57
53
  ]
58
54
  },
59
55
  "dependencies": {
60
56
  "@xyz-agent/session-delivery": "0.3.0",
61
- "@zhushanwen/pi-extension-logger": "0.3.0",
62
- "@xyz-agent/extension-protocol": "0.7.0",
63
- "@zhushanwen/subagent-core": "0.2.0"
57
+ "@zhushanwen/pi-ext-guards": "0.2.0",
58
+ "@zhushanwen/pi-extension-logger": "0.4.0",
59
+ "@xyz-agent/extension-protocol": "0.8.0",
60
+ "@zhushanwen/subagent-core": "0.4.0"
64
61
  },
65
62
  "peerDependencies": {
66
63
  "@earendil-works/pi-ai": "^0.84.4",
67
64
  "@earendil-works/pi-coding-agent": "^0.84.4",
68
65
  "@earendil-works/pi-tui": "^0.84.4",
69
66
  "typebox": "*",
70
- "@zhushanwen/pi-pending-notifications": "0.5.0",
71
- "@zhushanwen/pi-structured-output": "5.1.0"
67
+ "@zhushanwen/pi-structured-output": "5.1.2",
68
+ "@zhushanwen/pi-pending-notifications": "0.5.2"
72
69
  },
73
70
  "peerDependenciesMeta": {
74
71
  "@earendil-works/pi-coding-agent": {
@@ -39,13 +39,22 @@ This is enforced by `lintScript`:
39
39
 
40
40
  ## Required: Meta Declaration
41
41
 
42
- Every script MUST declare `meta` at the top level:
42
+ Every script MUST declare its metadata as a `/* @pi-meta */` YAML block comment at the top level — NOT a `const meta` variable:
43
43
 
44
44
  ```javascript
45
- const meta = { name: 'workflow-name', description: '...', phases: ['phase1', 'phase2'] };
45
+ /* @pi-meta
46
+ name: workflow-name
47
+ description: One-line description of what the workflow does
48
+ phases: ['phase1', 'phase2']
49
+ */
46
50
  ```
47
51
 
48
- `name` must match the filename stem. `phases` is for display only.
52
+ - `name` must match the filename stem. `phases` is for display only.
53
+ - Optional fields: `parameters` (JSON Schema for `$ARGS`) and `usage` (markdown).
54
+ - The YAML body starts on the line after `/* @pi-meta`; the closing `*/` must sit alone at column 0.
55
+ - `generate` round-trip-validates the YAML and reports line/col on error (common pitfall: patternProperties regex must use double backslash `\\d`, not `\d`).
56
+
57
+ > **Legacy `const meta = { ... }` (transition only):** `generate` still accepts a legacy top-level `const meta` during the transition window, but the core discovery chain does NOT recognize it (no legacy fallback) — such scripts stay invisible to discovery and get no metadata injection. Always use `/* @pi-meta */` for new scripts.
49
58
 
50
59
  ## [MANDATORY] Display: description + phase
51
60
 
@@ -59,11 +68,11 @@ TUI `/workflows` 视图按**运行时 `phase()` 调用**分组(非 `meta.phase
59
68
  ### Minimal 示例(三者齐备)
60
69
 
61
70
  ```javascript
62
- const meta = {
63
- name: 'review-fix',
64
- description: 'review then fix',
65
- phases: ['review', 'fix'],
66
- };
71
+ /* @pi-meta
72
+ name: review-fix
73
+ description: review then fix
74
+ phases: ['review', 'fix']
75
+ */
67
76
 
68
77
  phase('review');
69
78
  const r = await agent({ prompt: 'review diff', description: 'review-diff' });
@@ -79,11 +88,17 @@ return { done: true };
79
88
  // ❌ agent() 无 description → TUI 显示 unnamed agent
80
89
  await agent({ prompt: 'review diff' });
81
90
 
82
- // ❌ meta.phases 用对象数组 → 引擎忽略,全部归入 (unnamed)
83
- const meta = { name: 'x', phases: [{ title: 'review' }, { title: 'fix' }] };
91
+ // ❌ phases 用对象数组 → 引擎忽略,全部归入 (unnamed)
92
+ /* @pi-meta
93
+ name: x
94
+ phases: [{ title: 'review' }, { title: 'fix' }]
95
+ */
84
96
 
85
97
  // ❌ 声明了 phases 但从不 phase() 调用 → 运行时分组失效
86
- const meta = { name: 'x', phases: ['review', 'fix'] };
98
+ /* @pi-meta
99
+ name: x
100
+ phases: ['review', 'fix']
101
+ */
87
102
  // ... 直接 await agent(...) 从不调 phase('review') / phase('fix')
88
103
  ```
89
104
 
@@ -201,7 +216,7 @@ const results = await parallel(
201
216
  );
202
217
  ```
203
218
 
204
- > 内置通用编排 workflow(chain / parallel / scatter-gather / map-reduce,可直接 `workflow run`,用 `agent()` 自包含实现)见 `extensions/universal/subagent-workflow/workflows/`。本段教 `workflow()` 嵌套 API,workflows 目录是开箱即用的通用编排工具(用 `agent()` 而非 `workflow()` 嵌套)。
219
+ > 内置通用编排 workflow(chain / parallel / scatter-gather / map-reduce / review-fix-loop,可直接 `workflow run`,用 `agent()` 自包含实现)见 `extensions/universal/subagent-workflow/workflows/`。本段教 `workflow()` 嵌套 API,workflows 目录是开箱即用的通用编排工具(用 `agent()` 而非 `workflow()` 嵌套)。
205
220
 
206
221
  ### Other globals
207
222
 
@@ -249,7 +264,11 @@ agent({ prompt: '...', description: 'review-business-logic' });
249
264
  ## Complete Example
250
265
 
251
266
  ```javascript
252
- const meta = { name: 'review-fix-loop', description: 'Loop: review → fix → commit until clean', phases: ['review-fix'] };
267
+ /* @pi-meta
268
+ name: review-fix-loop
269
+ description: "Loop: review → fix → commit until clean"
270
+ phases: ['review-fix']
271
+ */
253
272
 
254
273
  const MAX_ROUNDS = 10;
255
274
  let round = 0;
@@ -12,7 +12,9 @@
12
12
  // mock 返回 {count: n} 形状直接验证「适配读 .count」;session-delivery mock
13
13
  // 验证 createDelivery 透传(参数 + 返回句柄)。
14
14
 
15
- import { join } from "node:path";
15
+ import { join, dirname } from "node:path";
16
+ import { createRequire } from "node:module";
17
+ import { existsSync } from "node:fs";
16
18
 
17
19
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
18
20
 
@@ -72,17 +74,36 @@ describe("createPiHostServices.discoveryRoots(与现推导逐项一致)", ()
72
74
  vi.mocked(getAgentDir).mockReturnValue(AGENT_DIR);
73
75
  }
74
76
 
75
- it("agents 三根:顺序与 source 标签 = user-pi → npm → npm-dev", () => {
77
+ it("agents 四根:user-pi → npm → npm-dev → core 包父目录(C5⑥,source npm 追加末位)", () => {
76
78
  stubAgentDir();
77
79
  const roots = createPiHostServices().discoveryRoots?.().agents;
78
80
 
81
+ // 期望的 core 注入根用与 pi-host.ts 相同的锚点解析(./workflows/* 子入口在
82
+ // workspace 与 npm dist 双形态同径;探针证据见 probe-c5.md P1/P3)
83
+ const require = createRequire(import.meta.url);
84
+ const anchor = require.resolve("@zhushanwen/subagent-core/workflows/README.md");
85
+ const coreParent = dirname(dirname(dirname(anchor)));
86
+
79
87
  expect(roots).toEqual([
80
88
  { dir: join(AGENT_DIR, "agents"), source: "user-pi" },
81
89
  { dir: join(AGENT_DIR, "npm", "node_modules"), source: "npm" },
82
90
  { dir: join(AGENT_DIR, "extensions"), source: "npm-dev" },
91
+ // C5⑥:core 包一级父目录(npm 槽语义:一级子项 = 包目录,无 pi manifest 扫
92
+ // agents/ 约定目录);追加在既有 npm 根之后——同标签靠后者胜(新版遮蔽旧残留)
93
+ { dir: coreParent, source: "npm" },
83
94
  ]);
84
95
  });
85
96
 
97
+ it("agents 第 4 根指向 core 包的父目录(其下 subagent-core/agents/ 存在 10 内置角色)", () => {
98
+ stubAgentDir();
99
+ const roots = createPiHostServices().discoveryRoots?.().agents;
100
+ const coreRoot = roots?.[3];
101
+
102
+ expect(coreRoot?.source).toBe("npm");
103
+ // 便捷断言:core 包根(注入 dir 的子目录)下 agents/ 真实存在
104
+ expect(existsSync(join(coreRoot!.dir, "subagent-core", "agents"))).toBe(true);
105
+ });
106
+
86
107
  it("workflows 三根:与 agents 同构,末级目录名切换为 workflows", () => {
87
108
  stubAgentDir();
88
109
  const roots = createPiHostServices().discoveryRoots?.().workflows;
@@ -16,7 +16,9 @@
16
16
  // buildScanTargets、orchestration/skill-discovery.ts resolveSkillPath 的现推导
17
17
  // 逐项一致(user-pi / npm / npm-dev 字面即现 ResourceSource 标签)。
18
18
  // project/workspace 根不在壳侧提供——core 消费方按 workspaceRoot 自行推导
19
- // (u0-data-discovery 波次接注入消费)。
19
+ // (u0-data-discovery 波次接注入消费)。C5⑥ 起 agents kind 追加第 4 根:
20
+ // core 包一级父目录(source "npm")——core agents/ 资产进 pi 发现面,见
21
+ // corePackageNpmRoot 注释。
20
22
  // - countActiveFromEntries 适配:pi 侧真函数返回 CountActiveResult 对象,core
21
23
  // 端口契约是 number(core 消费面只读 .count,notify-ports.ts 契约注释)——
22
24
  // foundation 单元登记给本单元的适配责任。
@@ -25,7 +27,8 @@
25
27
  // 是结构超集成员,多不碍兼容)——结构兼容由本注入点 typecheck 守护,上游签名
26
28
  // 漂移即红(notify-ports.ts「闭包红线」段)。
27
29
 
28
- import { join } from "node:path";
30
+ import { createRequire } from "node:module";
31
+ import { dirname, join } from "node:path";
29
32
 
30
33
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
31
34
  import { getLogger } from "@zhushanwen/pi-extension-logger";
@@ -36,12 +39,50 @@ import type { DiscoveryRoot, HostServices } from "@zhushanwen/subagent-core/core
36
39
  import type { LogLevel } from "@zhushanwen/subagent-core/core/logger.ts";
37
40
  import type { NotifyDomainPorts } from "@zhushanwen/subagent-core/core/notify-ports.ts";
38
41
 
42
+ /**
43
+ * core 包(@zhushanwen/subagent-core)agents/ 资产进 pi 发现面的注入根(C5⑥,
44
+ * convergence §5.4 检查点 2;探针证据 docs/design/subagent-core-convergence.probe-c5.md)。
45
+ *
46
+ * 解析锚点 = `@zhushanwen/subagent-core/workflows/README.md`:`./workflows/*` 子入口
47
+ * 在 workspace TS 直引与 npm dist 两种发布形态下同径(publishConfig 保留该子入口),
48
+ * README.md 是两形态都必在的资产文件。core 包根 = 锚点上两级;npm 槽注入其一级
49
+ * 父目录——npm 槽语义:dir 下一级子项 = 包目录,core 无 pi manifest → 扫 agents/
50
+ * 约定目录命中 10 内置角色。
51
+ *
52
+ * 布局覆盖(探针 P2-P5 实测):dev workspace(core 在仓库 packages/ 下)与发布态
53
+ * 嵌套布局(core 在本包 node_modules 内)下这是唯一命中通路;发布态平铺布局
54
+ * (core 与本包同层)下既有 npm 根已命中,本注入是幂等兜底(重复发现被 core
55
+ * realpath 去重吸收)。
56
+ *
57
+ * 每次调用现解析(不 memo):发现调用点稀疏(session_start + 缓存 miss),解析
58
+ * 成本可忽略;失败(异常布局/解析器不可用)降级为不注入并 warn——绝不因资产
59
+ * 接线失败阻断发现主链。
60
+ */
61
+ function corePackageNpmRoot(): string | undefined {
62
+ try {
63
+ // createRequire 锚定本模块(jiti 加载器下 import.meta.url 可用;不可用则随
64
+ // catch 降级)。require.resolve 沿 pi-sw 自身的依赖解析链——workspace 与发布态
65
+ // 都从本包出发命中 core。
66
+ const require = createRequire(import.meta.url);
67
+ const anchor = require.resolve("@zhushanwen/subagent-core/workflows/README.md");
68
+ return dirname(dirname(dirname(anchor)));
69
+ } catch (err) {
70
+ // getLogger 惰性调用(catch 是冷路径——测试环境对 pi-extension-logger 的
71
+ // module-level mock 可能返回 undefined,模块级持有会在 import 期踩 undefined)
72
+ getLogger("pi-host").warn(
73
+ "[pi-host] core 包 agents/ 注入根解析失败——10 内置角色可能不可发现",
74
+ { reason: err instanceof Error ? err.message : String(err) },
75
+ );
76
+ return undefined;
77
+ }
78
+ }
79
+
39
80
  /** agents/workflows 共享的 agentDir 派生根(末级目录名由 kind 决定)。
40
81
  * 顺序与 source 标签逐项对齐 resource-discovery.ts buildScanTargets 的
41
82
  * user-pi → npm → npm-dev 段(根列表按优先级低→高排列,D2 语义边界)。 */
42
83
  function agentDirKindRoots(kind: "agents" | "workflows"): DiscoveryRoot[] {
43
84
  const agentDir = getAgentDir();
44
- return [
85
+ const roots: DiscoveryRoot[] = [
45
86
  // 1. user .pi/agent/<kind>/
46
87
  { dir: join(agentDir, kind), source: "user-pi" },
47
88
  // 2. npm global: agentDir/npm/node_modules/*/<pkg>/
@@ -49,6 +90,19 @@ function agentDirKindRoots(kind: "agents" | "workflows"): DiscoveryRoot[] {
49
90
  // 3. npm dev symlink: agentDir/extensions/*/<pkg>/
50
91
  { dir: join(agentDir, "extensions"), source: "npm-dev" },
51
92
  ];
93
+ // 4. core 包根(C5⑥,仅 agents kind):追加在既有 npm 根之后——同标签多根依注入
94
+ // 序扫描 + last-writer-wins,core(随本包依赖分发的新模板)遮蔽同 agentDir 内
95
+ // 旧版残留副本;序位仍在 user 级之上、npm-dev/project 级之下(红线 1)。
96
+ // workflows kind 刻意不注入:<available_workflows> 的 <location> 是 CA2 快照
97
+ // 不豁免面(红线 8 豁免仅限 10 内置 agent 角色路径前缀),注入会翻转内置
98
+ // workflow 的胜出路径。
99
+ if (kind === "agents") {
100
+ const coreNpmRoot = corePackageNpmRoot();
101
+ if (coreNpmRoot !== undefined) {
102
+ roots.push({ dir: coreNpmRoot, source: "npm" });
103
+ }
104
+ }
105
+ return roots;
52
106
  }
53
107
 
54
108
  /** skills 的 agentDir 派生根。对齐 skill-discovery.ts resolveSkillPath 的两处
package/src/index.ts CHANGED
@@ -25,6 +25,7 @@ import { configureCore } from "@zhushanwen/subagent-core/core/host-services.ts";
25
25
  import { configureNotifyDomain } from "@zhushanwen/subagent-core/core/notify-ports.ts";
26
26
  import { createPiHostServices, createPiNotifyDomainPorts } from "./host/pi-host.ts";
27
27
 
28
+ import { oncePerProcess } from "@zhushanwen/pi-ext-guards";
28
29
  import { bestEffort } from "@zhushanwen/subagent-core/execution/best-effort.ts";
29
30
  // ═══ execution/ 层(subagents 核心 + 运行时) ═══
30
31
  import { getOrCreateChannelRegistry } from "@zhushanwen/subagent-core/execution/channel-registry-access.ts";
@@ -49,7 +50,7 @@ import {
49
50
  } from "@zhushanwen/subagent-core/execution/model-config-service.ts";
50
51
  import { bindNotifyLedgerHost, getBoundNotifyLedger, type NotifyLedgerHost } from "@zhushanwen/subagent-core/execution/notify-ledger.ts";
51
52
  import { IDENTITY_CUSTOM_TYPE, type SubagentIdentityData } from "@zhushanwen/subagent-core/execution/session-reconstructor.ts";
52
- import type { ExecutionMode, SubagentRecord } from "@zhushanwen/subagent-core/execution/types.ts";
53
+ import type { ExecutionMode } from "@zhushanwen/subagent-core/execution/types.ts";
53
54
  import { maybeCleanupExpiredSessionFiles } from "@zhushanwen/subagent-core/execution/session-file-gc.ts";
54
55
  import {
55
56
  getSubagentService,
@@ -59,8 +60,10 @@ import {
59
60
  import { killAllSpawnedChildren } from "@zhushanwen/subagent-core/execution/session-runner.ts";
60
61
  import { SubprocessAgentRunner } from "@zhushanwen/subagent-core/execution/subprocess-agent-runner.ts";
61
62
  import { WorktreeManager } from "@zhushanwen/subagent-core/execution/worktree-manager.ts";
62
- // [engine-awareness U3] per-turn 引擎检测编排(D1/D1b/D2/D3/D5
63
- import { normalizeEngineId, runEngineAwarenessTurn } from "./injectors/engine-awareness.ts";
63
+ // [engine-awareness U3] per-turn 引擎检测编排(D1/D1b/D2/D3/D5);normalizeEngineId
64
+ // 单一权威源在 core registry(原经 engine-awareness 再导出,导入面已折叠直连)
65
+ import { normalizeEngineId } from "@zhushanwen/subagent-core/execution/engine/registry.ts";
66
+ import { runEngineAwarenessTurn } from "./injectors/engine-awareness.ts";
64
67
  import { setupModelListInjector } from "./injectors/model-list-injector.ts";
65
68
  import { setupSubagentListInjector } from "./injectors/subagent-list-injector.ts";
66
69
  import { setupWorkflowListInjector } from "./injectors/workflow-list-injector.ts";
@@ -81,6 +84,7 @@ import { executeNestedWorkflow, runAndWait, type WorkflowRunResult } from "@zhus
81
84
  import {
82
85
  evictDoneRunsBeyondCap,
83
86
  MAX_RETAINED_DONE_RUNS,
87
+ recoverCrashedRuns,
84
88
  scheduleTimeBudget,
85
89
  terminateRunningRuns,
86
90
  } from "@zhushanwen/subagent-core/orchestration/lifecycle.ts";
@@ -106,42 +110,6 @@ declare module "@earendil-works/pi-coding-agent" {
106
110
  // 模块级 logger(setPiHandle 注入后自动走 appendEntry)
107
111
  const logger = getLogger("subagents");
108
112
 
109
- // ── subagent 状态快照格式化 ──
110
- //
111
- // [v4 A-6] before_agent_start 注入 hook 已删(活跃 subagent 清单改由 agent 按需调
112
- // action:'list' 拉取,消除每 loop 注入的上下文税与盲点)。本函数保留为纯格式化工具:
113
- // before-agent-start-injection / parent-child-matrix 测试覆盖其正确性,未来 list
114
- // 视图或其他注入点可复用。
115
-
116
- /** 活跃 subagent 数量上限(超过截断显示)。 */
117
- const MAX_STATUS_INJECTION = 10;
118
-
119
- /**
120
- * 将活跃 subagent record 格式化为一行一条的快照文本。
121
- *
122
- * 格式:
123
- * [subagent-status] N active subagents:
124
- * - sa-xxx (slug): running, rounds 0
125
- * - sa-yyy (slug): idle, rounds 3
126
- * +2 more, use action:'list'
127
- *
128
- * @param records 已筛选的活跃 record(running + idle)
129
- */
130
- export function formatSubagentStatusSnapshot(records: SubagentRecord[]): string {
131
- const lines = [`[subagent-status] ${records.length} active subagent${records.length === 1 ? "" : "s"}:`];
132
- const shown = records.slice(0, MAX_STATUS_INJECTION);
133
- for (const r of shown) {
134
- const slug = r.slug || r.agent;
135
- const roundPart = r.round !== undefined && r.round > 0 ? `, rounds ${r.round}` : "";
136
- lines.push(`- ${r.id} (${slug}): ${r.status}${roundPart}`);
137
- }
138
- const remaining = records.length - MAX_STATUS_INJECTION;
139
- if (remaining > 0) {
140
- lines.push(`+${remaining} more, use action:'list'`);
141
- }
142
- return lines.join("\n");
143
- }
144
-
145
113
  // ═══ [V2 决策 7 防线 i] process 级 shutdown hook ═══
146
114
  //
147
115
  // session_shutdown 是 pi 的 async hook,进程被 SIGTERM/SIGINT 强杀或崩溃时来不及
@@ -248,12 +216,10 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
248
216
  }
249
217
  }
250
218
 
251
- // resources_discover:不再注入额外 skill 目录(ADR-031 废弃 discovery.json)。
252
- // pi 核心 auto-discovery 已覆盖 .agents/skills 等标准目录,子 session 的
253
- // --skill 由 agent({skill}) 调用方显式传入,无需 extension 额外补充。
254
- pi.on("resources_discover", (_event, _ctx: ExtensionContext) => {
255
- return {};
256
- });
219
+ // resources_discover:不再注册 handler(v4 决策:不再注入额外 skill 目录,
220
+ // ADR-031 废弃 discovery.json)。pi 核心 auto-discovery 已覆盖 .agents/skills
221
+ // 等标准目录,子 session 的 --skill 由 agent({skill}) 调用方显式传入,无需
222
+ // extension 额外补充。
257
223
 
258
224
  // ════════════════════════════════════════════════════════════
259
225
  // workflow 域:tools + command + pi.__workflowRun + state
@@ -381,8 +347,9 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
381
347
  lsRef.lastSessionId = sessionId;
382
348
 
383
349
  // [U7] 引擎列表同步 engines.json(幂等零写 + fail-safe;组合根注册已在
384
- // extension 工厂体完成,此处 registry 已含全部引擎)
385
- syncEnginesFile(agentDir);
350
+ // extension 工厂体完成,此处 registry 已含全部引擎)。写 agentDir 全局文件属
351
+ // 跨 session 副作用——oncePerProcess 守卫防 factory 二调/handler 累积双跑(u-audit-fix)。
352
+ oncePerProcess("subagent-workflow:sync-engines-file", () => syncEnginesFile(agentDir));
386
353
 
387
354
  // skill 路径两级缓存 session 级失效:pi 同进程可能有多个 session(TUI /new、/fork),
388
355
  // 运行中安装的 skill 需对新 session 可见(含曾 miss 缓存的 undefined 条目与 npm 新装
@@ -527,21 +494,27 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
527
494
  setSubagentService(service);
528
495
  }
529
496
 
530
- // S-2: 启动 idle record GC 定时器(30 天 TTL,每小时检查一次)
531
- service.startGcTimer();
497
+ // S-2: 启动 idle record GC 定时器(30 天 TTL,每小时检查一次)。
498
+ // 注册 setInterval 属进程级副作用——oncePerProcess 守卫防双跑(u-audit-fix)。
499
+ oncePerProcess("subagent-workflow:start-gc-timer", () => service.startGcTimer());
532
500
 
533
501
  try {
534
- maybeCleanupExpiredSessionFiles(agentDir, cwd);
502
+ // 递归扫描 <agentDir>/subagents + unlink 超 TTL 跨 session 文件属进程级维护
503
+ // ——oncePerProcess 守卫防双跑(u-audit-fix)。
504
+ oncePerProcess("subagent-workflow:cleanup-expired-session-files", () =>
505
+ maybeCleanupExpiredSessionFiles(agentDir, cwd));
535
506
  } catch (err) {
536
507
  logger.warn("[subagents] expired session file cleanup failed", {
537
508
  reason: err instanceof Error ? err.message : String(err),
538
509
  });
539
510
  }
540
511
 
541
- // ADR-035 启动恢复:扫描 manifest tmp 残留(崩溃打断的 writeManifest 留下),
542
- // 每次 session_start 都调(与上方 maybeCleanupExpiredSessionFiles 一致)。
512
+ // ADR-035 启动恢复:扫描 manifest tmp 残留(崩溃打断的 writeManifest 留下,promote/unlink)。
513
+ // 扫描属进程级维护——oncePerProcess 守卫防双跑(u-audit-fix);第二派发重放首次
514
+ // Promise(结果缓存语义),recovered 计数日志可能重打,无文件副作用。
543
515
  try {
544
- const recovered = await service.recoverManifestTmpFiles();
516
+ const recovered = await oncePerProcess("subagent-workflow:recover-manifest-tmp-files", () =>
517
+ service.recoverManifestTmpFiles());
545
518
  if (recovered.recovered > 0 || recovered.deleted > 0) {
546
519
  logger.warn(`[subagents] manifest tmp recovery: ${recovered.recovered} promoted, ${recovered.deleted} deleted`);
547
520
  }
@@ -552,8 +525,9 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
552
525
  }
553
526
 
554
527
  try {
555
- const wtm = new WorktreeManager(agentDir);
556
- await wtm.scan();
528
+ // 孤儿 worktree 清理(git/rm 进程操作 + 注册表/目录扫描)属进程级维护
529
+ // ——oncePerProcess 守卫防双跑(u-audit-fix)。
530
+ await oncePerProcess("subagent-workflow:worktree-scan", () => new WorktreeManager(agentDir).scan());
557
531
  } catch (err) {
558
532
  logger.warn("[subagents] worktree reaper scan failed", {
559
533
  reason: err instanceof Error ? err.message : String(err),
@@ -575,49 +549,30 @@ export default function subagentsWorkflowExtension(pi: ExtensionAPI): void {
575
549
  // resolveIdentity),无需经 state 透传——modelService 是唯一 registry 源。
576
550
 
577
551
  // MF-1: store 健康度跟踪。loadAll 失败 → storeHealthy=false,workflow 域启动时 fail-fast。
552
+ // 崩溃恢复四步(loadAll → failed → save → evict)收口到 core recoverCrashedRuns(D8:
553
+ // 宿主各写一遍正是 failure-mode-B);pending:unregister 经 hooks 外置发射(位置在
554
+ // transition 后、save 前,对齐原内联实现);save 走 store 冷路径(done 绕过去抖)——
555
+ // 冷路径语义在 JsonlRunStore.save 内,不随循环归属转移。loadAll 失败的 fail-fast
556
+ // (storeHealthy=false 停初始化)是宿主职责,core 原样上抛、这里 catch 兜住。
578
557
  let storeHealthy = true;
579
558
  try {
580
- const loaded = await store.loadAll();
581
- for (const run of loaded) {
582
- if (run.state.status === "running") {
583
- run.state.error = "Process killed (kill-9 or crash recovery)";
584
- run.transition("done", "failed");
585
- pi.events.emit("pending:unregister", {
586
- id: run.runId,
587
- reason: "failed",
588
- });
589
- // 恢复终态必须落盘:save 走冷路径(done 绕过去抖)同步写 state 文件 +
590
- // append 终态 workflow-record entry——entry_appended 事件驱动 runtime 派生
591
- // 缓存失效重拉(无 triggerTurn 副作用)。不 save 则 entry/state 双双停留
592
- // running,侧栏永久卡 running。失败仅记日志不阻断其余 run 的恢复(下次
593
- // session_start 重开重试,恢复循环天然幂等)。
594
- try {
595
- await store.save(run);
596
- } catch (err) {
597
- logger.error("[subagent-workflow] kill-9 recovery store.save failed", {
598
- runId: run.runId,
599
- reason: err instanceof Error ? err.message : String(err),
600
- });
601
- }
602
- }
603
- runs.set(run.runId, run);
604
- }
605
- // done run 内存有界性:loadAll 全量重水合后立即裁剪到 K。kill-9 恢复(上方
606
- // running → transition("done","failed"))的 run completedAt 为 transition 时刻
607
- // (当前时间=全局最新)参与排序且必在保留端;多条恢复 run 同 ms completedAt →
608
- // tie 稳定排序。淘汰只 delete runs Map 条目——磁盘 state 文件与
609
- // workflow-state-link 指针条目均不动(历史审计保留);下次 session_start loadAll
610
- // 从指针全量重水合后再次裁剪,该循环每次 session 启动重复且可接受:内存峰值只在
611
- // 启动期,常驻 O(K + 活跃 run)。消除启动峰值需指针 compaction,属 append+replay
612
- // 长期方案问题域,非本范围。
613
- const evicted = evictDoneRunsBeyondCap(runs, MAX_RETAINED_DONE_RUNS);
614
- if (evicted > 0) {
615
- logger.debug("[subagent-workflow] evicted done runs beyond cap after loadAll", {
616
- evicted,
617
- keep: MAX_RETAINED_DONE_RUNS,
618
- sessionId,
619
- });
620
- }
559
+ // 崩溃恢复 loadAll cwd 共享 sessionDir(同 cwd 跨 session 共享)并把 running run
560
+ // failed 落盘——写非本 session 的 run state 文件属跨 session 副作用,oncePerProcess
561
+ // 守卫防双跑(u-audit-fix)。第二派发重放首次 Promise:不再落盘、不再 emit。
562
+ await oncePerProcess(
563
+ "subagent-workflow:recover-crashed-runs",
564
+ () =>
565
+ recoverCrashedRuns(
566
+ store,
567
+ runs,
568
+ "Process killed (kill-9 or crash recovery)",
569
+ {
570
+ onRunRecovered: (payload) => {
571
+ pi.events.emit("pending:unregister", payload);
572
+ },
573
+ },
574
+ ),
575
+ );
621
576
  } catch (err) {
622
577
  // QMF-4 fix: store.loadAll 失败是关键路径错误,workflow 域将未初始化
623
578
  logger.error("[subagent-workflow] store.loadAll failed, workflow domain uninitialized", {
@@ -997,17 +952,8 @@ function getOrCreateDialogQueue(): DialogGlobalQueue {
997
952
  return queue;
998
953
  }
999
954
 
1000
- // ============================================================
1001
- // Public cross-extension API(channel handler 注册入口)
1002
- // ============================================================
1003
- //
1004
- // 跨扩展消费者(ask-user 等)通过包根 import 注册 channel handler,
1005
- // 让 subagent 子进程的 UI 请求(ask_user 等)透传到主进程渲染。
1006
- // 重新导出 channel-registry-access 的公开 API——稳定 surface,
1007
- // 内部存储实现演进不影响消费者。
1008
-
1009
- export {
1010
- getOrCreateChannelRegistry,
1011
- type UiChannelRegistry,
1012
- type ChannelHandler,
1013
- } from "@zhushanwen/subagent-core/execution/channel-registry-access.ts";
955
+ // 跨扩展 channel handler 注册入口已收口到 core 深路径
956
+ // `@zhushanwen/subagent-core/execution/channel-registry-access.ts`
957
+ // (getOrCreateChannelRegistry / UiChannelRegistry / ChannelHandler)。
958
+ // 历史上的包根 re-export 已删:ask-user 等跨扩展消费者经 globalThis 握手
959
+ // (DIALOG_QUEUE_KEY 同款进程级单例),不再经包根 import 消费本模块。
@@ -1,7 +1,7 @@
1
1
  // engine-awareness 单测([engine-awareness U3])
2
2
  //
3
3
  // 覆盖(设计 docs/design/subagent-engine-awareness-injection.md 验收挂钩 D1/D1b/D2/D3/D5):
4
- // 1. normalizeEngineId:缺省/空白归一到 'pi'(单一权威源 registry.ts,经本模块再导出)
4
+ // 1. normalizeEngineId:缺省/空白归一到 'pi'(单一权威源 registry.ts,直连导入)
5
5
  // 2. buildEngineChangeNotice:§3.1 文案骨架、pi/非 pi 指路段分界、不含任何模型清单(D4)
6
6
  // 3. runEngineAwarenessTurn 编排:
7
7
  // - 变更触发 apply + 通知(D2 顺序硬约束:提交缓存先于通知先于记账)
@@ -18,11 +18,11 @@ import { describe, expect, it } from "vitest";
18
18
  import {
19
19
  buildEngineChangeNotice,
20
20
  ENGINE_CHANGE_CUSTOM_TYPE,
21
- normalizeEngineId,
22
21
  runEngineAwarenessTurn,
23
22
  type EngineAwarenessDeps,
24
23
  type EngineAwarenessOutcome,
25
24
  } from "../engine-awareness";
25
+ import { normalizeEngineId } from "@zhushanwen/subagent-core/execution/engine/registry.ts";
26
26
  import type { GlobalConfigReadResult } from "@zhushanwen/subagent-core/execution/config.ts";
27
27
 
28
28
  // ── 测试数据 ────────────────────────────────────────────
@@ -26,7 +26,9 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest";
26
26
 
27
27
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
28
28
 
29
- import { formatModelList, setupModelListInjector, type ModelEntry } from "../model-list-injector.ts";
29
+ // C5①:formatModelList/ModelEntry 下沉 core barrel(setupModelListInjector injector
30
+ import { formatModelList, type ModelEntry } from "@zhushanwen/subagent-core";
31
+ import { MODEL_LIST_GUIDE, setupModelListInjector } from "../model-list-injector.ts";
30
32
  import type { EnginePort } from "@zhushanwen/subagent-core/execution/engine/port.ts";
31
33
  import { clearEngines, registerEngine } from "@zhushanwen/subagent-core/execution/engine/registry.ts";
32
34
  import {
@@ -80,7 +82,7 @@ const APPEND_SEPARATOR = "\n\n";
80
82
 
81
83
  /** 合成一个 turn 的 system prompt 尾部:BASE + provider models 段 + engine 追加段。 */
82
84
  function composeTurn(defaultEngine: string | undefined): string {
83
- const providerSection = formatModelList(PROVIDER_ENTRIES);
85
+ const providerSection = formatModelList(PROVIDER_ENTRIES, { guide: MODEL_LIST_GUIDE });
84
86
  const afterProvider = providerSection === "" ? BASE : BASE + providerSection;
85
87
  const append = composeEngineAppend(defaultEngine);
86
88
  return append === "" ? afterProvider : `${afterProvider}${APPEND_SEPARATOR}${append}`;
@@ -190,7 +192,7 @@ describe("段渲染确定性(D7:同输入恒同输出)", () => {
190
192
  * - 切换后的引擎追加段恰居 prompt 尾部。
191
193
  */
192
194
  function expectOnlyTailDiffers(from: string | undefined, to: string | undefined): void {
193
- const stableHead = BASE + formatModelList(PROVIDER_ENTRIES);
195
+ const stableHead = BASE + formatModelList(PROVIDER_ENTRIES, { guide: MODEL_LIST_GUIDE });
194
196
  const before = composeTurn(from);
195
197
  const after = composeTurn(to);
196
198
  const beforeAppend = composeEngineAppend(from);
@@ -279,7 +281,7 @@ describe("段序守护:engine 段恒链尾(D7)", () => {
279
281
  // 恒链尾:engine 追加段是 prompt 的最后内容
280
282
  expect(prompt.endsWith(`${APPEND_SEPARATOR}${composeEngineAppend(defaultEngine)}`)).toBe(true);
281
283
  // model list handler 注入形态锚定(BASE + injection 无分隔,生产 handler 同款)
282
- expect(prompt.startsWith(BASE + formatModelList(PROVIDER_ENTRIES))).toBe(true);
284
+ expect(prompt.startsWith(BASE + formatModelList(PROVIDER_ENTRIES, { guide: MODEL_LIST_GUIDE }))).toBe(true);
283
285
  });
284
286
 
285
287
  it("对照:注册序颠倒(engine 先、model list 后)时段序断言必失败——守护有判别力", async () => {