@zhushanwen/pi-subagent-workflow 8.8.0 → 8.8.1

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 (35) hide show
  1. package/package.json +8 -8
  2. package/src/host/pi-host.ts +5 -4
  3. package/src/index.ts +145 -464
  4. package/src/injectors/__tests__/engine-awareness.test.ts +4 -2
  5. package/src/injectors/__tests__/engine-section-stability.test.ts +42 -29
  6. package/src/injectors/engine-awareness.ts +80 -4
  7. package/src/injectors/resource-list-injector.ts +209 -0
  8. package/src/injectors/subagent-list-injector.ts +30 -143
  9. package/src/injectors/workflow-list-injector.ts +23 -150
  10. package/src/interface/__tests__/detectors.test.ts +1 -1
  11. package/src/interface/__tests__/subagent-tool-path-guard.test.ts +42 -26
  12. package/src/interface/__tests__/subagent-tool-prompt.test.ts +10 -2
  13. package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +1 -1
  14. package/src/interface/bg-notify-render.ts +4 -4
  15. package/src/interface/commands.ts +115 -75
  16. package/src/interface/format.ts +339 -241
  17. package/src/interface/helpers.ts +2 -2
  18. package/src/interface/list-component.ts +163 -116
  19. package/src/interface/list-shared.ts +2 -2
  20. package/src/interface/list-view.ts +129 -72
  21. package/src/interface/subagent-actions.ts +5 -5
  22. package/src/interface/subagent-tool-schema.ts +6 -5
  23. package/src/interface/subagent-tool.ts +4 -3
  24. package/src/interface/subagents.ts +8 -7
  25. package/src/interface/tool-render.ts +2 -2
  26. package/src/interface/tool-workflow-script.ts +15 -14
  27. package/src/interface/tool-workflow.ts +14 -11
  28. package/src/interface/tui-kit.ts +305 -0
  29. package/src/interface/views/WorkflowsView.ts +267 -196
  30. package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +3 -3
  31. package/src/interface/views/__tests__/detail-content-session-file.test.ts +7 -4
  32. package/src/interface/views/detail-content.ts +16 -14
  33. package/src/interface/views/view-constants.ts +19 -0
  34. package/src/jsonl-run-store.ts +11 -7
  35. package/src/interface/views/format.ts +0 -281
@@ -6,21 +6,17 @@
6
6
  * 名字而非臆造。注入格式对齐 pi 内置 skill 注入(XML 标签)。
7
7
  *
8
8
  * 改造点:
9
- * - P4:发现路径由自实现扫 4 目录改为同包 ADR-031 统一发现 discoverResources
10
- * (P4 时点覆盖 7 源;C2 扩源后为统一发现全源 9 值 ResourceSource,含
11
- * user-extension-paths/project-pi-tmp/project-host——序见 resource-discovery.ts
12
- * 模块头注 + 优先级合并)。discoverResources 只返回 DiscoveredResource(path/source/
13
- * available),不含 name/description——保留 parseAgentFrontmatter 解析每个 .md 的
14
- * frontmatter 提取 name+description。
9
+ * - D7-②(dual-track convergence):同构骨架(缓存对/唯一写点/三 handler)收敛到
10
+ * 同目录 resource-list-injector.ts 工厂,本文件只保留 agent 侧真差异。
15
11
  * - P3:注入段引导语含正向触发引导(何时该 delegate)+ 名字约束(文案见
16
12
  * SUBAGENT_LIST_GUIDE)。
17
13
  * - C5①(convergence D-3):formatAgentList/sortByCodepoint 下沉 core,本文件改调
18
14
  * core barrel(渲染骨架与条目模板逐字节同 pi 旧本地实现——CA2 快照验收前提);
19
15
  * guide 文案是 pi 宿主注入(core 不内嵌平台文案)。
20
16
  * - U11(sink 设计):装配循环(发现→解析→去重→排序 + warn/error 口径)整体改
21
- * 消费 core discoverAgents(execution/agents-assembly,U2/A6)——壳侧收缩为
22
- * 「宿主注入根现取 + 委托」,语义等值口径见 agents-assembly.ts 头注(⛔1 探针
23
- * 对照注入 XML diff 为空验收)。
17
+ * 消费 core discoverAgents(execution/agents-assembly,U2/A6)——经工厂 assemble
18
+ * 覆写槽委托,壳侧收缩为「宿主注入根现取 + 委托」,语义等值口径见
19
+ * agents-assembly.ts 头注(⛔1 探针对照注入 XML diff 为空验收)。
24
20
  *
25
21
  * 归位原因:injector 是 subagent-workflow 的内聚功能壳——事件接线与数据获取留
26
22
  * 在插件层(before_agent_start / modelRegistry),解析/渲染算法消费 core。
@@ -28,33 +24,15 @@
28
24
 
29
25
 
30
26
 
31
- import type {
32
- BeforeAgentStartEvent,
33
- BeforeAgentStartEventResult,
34
- ExtensionAPI,
35
- ExtensionContext,
36
- SessionShutdownEvent,
37
- SessionStartEvent,
38
- } from "@earendil-works/pi-coding-agent";
39
- import { getLogger } from "@zhushanwen/pi-extension-logger";
40
-
41
- import { getHostServices } from "@zhushanwen/subagent-core/core/host-services.ts";
42
- // U11(sink 设计):agent 装配循环单源 core discoverAgents(U2/A6 装配函数,
43
- // 深路径消费);渲染(formatAgentList)与 workspace 根推导(findWorkspaceRoot)
44
- // 走 barrel,parseResourceMeta 仍服务 parseAgentFrontmatter(严格注入投影语义锚)。
45
- // 上方 getHostServices 深路径刻意保留:core 服务定位器(core 内模块统一经它取用
46
- // 宿主端口),barrel 刻意不导出(D5 exports 面即 semver 契约、逐名列出,未列名
47
- // 的内部件不经 barrel——见 subagent-core src/index.ts 头注),壳侧深路径消费经包
48
- // `./*` -> src 通配豁免。
49
27
  import {
50
28
  type AgentEntry,
51
- findWorkspaceRoot,
29
+ discoverAgents,
52
30
  formatAgentList,
31
+ getHostServices,
53
32
  parseResourceMeta,
54
33
  } from "@zhushanwen/subagent-core";
55
- import { discoverAgents } from "@zhushanwen/subagent-core/execution/agents-assembly.ts";
56
34
 
57
- const logger = getLogger("injector");
35
+ import { createResourceListInjector } from "./resource-list-injector.ts";
58
36
 
59
37
  /**
60
38
  * pi 版注入引导文案(C5① guide 参数化——core 渲染函数不内嵌平台文案,宿主注入)。
@@ -68,37 +46,15 @@ const logger = getLogger("injector");
68
46
  export const SUBAGENT_LIST_GUIDE =
69
47
  "The following subagents are available. PRIORITY: when a task involves reading 3+ files, writing 100+ lines, parallel research, or specialized review, delegate to a matching subagent FIRST instead of doing it yourself — this keeps your context focused on orchestration. Do NOT call list to discover available subagents; use list only for running state. When using the subagent tool, ONLY use agents from this list — pass the <location> path (absolute .md path) as the agent param. If no agent matches your task, omit agent (a general-purpose agent is used) and put all role-specific instructions in the task text.";
70
48
 
71
- /**
72
- * Session 级 agent 列表缓存(per-process = per-session)。
73
- *
74
- * xyz-agent session-pool 模型下每 pi 子进程 = 一 session = 独立扩展实例,模块级缓存
75
- * 天然 per-session 隔离(split mode 多 session 各自独立进程)。session_start(含
76
- * reload)触发发现覆盖缓存(刷新节奏对齐 pi skill);session_shutdown 清空;
77
- * before_agent_start 读缓存,miss(session_start 未触发/缓存被清)则 fallback
78
- * 重新发现——保证鲁棒。
79
- */
80
- let agentCache: AgentEntry[] | null = null;
81
-
82
- /**
83
- * 注入块渲染缓存(与 agentCache 同步更新):before_agent_start 每个 turn 都要注入,
84
- * formatAgentList(escapeXml 多趟正则 × 全部字段)在数据不变时输出完全相同——渲染
85
- * 一次随缓存复用,turn 热路径零重复计算。
86
- */
87
- let agentInjectionCache: string | null = null;
88
-
89
- /** agentCache 唯一写点:数据与渲染缓存同步更新(null 清空两者)。 */
90
- function setAgentCache(entries: AgentEntry[] | null): void {
91
- agentCache = entries;
92
- agentInjectionCache =
93
- entries !== null ? formatAgentList(entries, { guide: SUBAGENT_LIST_GUIDE }) : null;
94
- }
95
-
96
49
  /**
97
50
  * 解析 markdown 文件的 YAML frontmatter(name + description)。
98
51
  *
99
52
  * m2 收敛:删本地单行 key:value parser,改调 shared/meta-parser.ts parseResourceMeta
100
53
  * (IF1 统一 parser,支持 block scalar)。无有效 frontmatter 或缺 name/description 返 null。
101
54
  * 投影 {name, description} 注入用(examples 注入留 m5)。
55
+ *
56
+ * U11 后装配循环(含本函数的装配内消费)单源 core discoverAgents;保留导出作为
57
+ * 严格注入投影语义锚(parseResourceMeta 消费面)。
102
58
  */
103
59
  export function parseAgentFrontmatter(content: string): AgentEntry | null {
104
60
  const meta = parseResourceMeta(content, "agent");
@@ -108,98 +64,29 @@ export function parseAgentFrontmatter(content: string): AgentEntry | null {
108
64
  description: meta.description,
109
65
  when: meta.when,
110
66
  examples: meta.examples,
111
- path: "", // 由 discoverAllAgents 从 DiscoveredResource.path 填充
67
+ path: "", // 装配时由 core discoverAgents 从 DiscoveredResource.path 填充
112
68
  };
113
69
  }
114
70
 
115
71
  /**
116
- * 发现所有可用 agent(注入清单装配入口)。
117
- *
118
- * U11(sink 设计):原手写装配循环(discoverResources 逐个 parseAgentFrontmatter
119
- * 解析 → agent name 去重(高优先级靠后 Map 后写胜)→ name 码点序排序 + warn/error
120
- * 口径)整体改消费 core `discoverAgents`(U2/A6 装配函数)——解析/去重/排序/warn
121
- * 口径单源 core,第三宿主免复刻(G3/S5)。等值口径(IF1 严格层可见性、warn 仅限
122
- * 「有 frontmatter 但解析失败」、KV-cache 码点序契约:注入段进每 turn system
123
- * prompt,顺序与 readdir 枚举序解耦,目录内容不变时任意重建渲染逐字节一致)登记见
124
- * core execution/agents-assembly.ts 头注。
125
- *
126
- * 壳侧保留职责:签名(workspaceRoot 单参)与宿主注入根现取(pi 壳 discoveryRoots
127
- * 每次现取 getAgentDir,实例隔离;agentDir 形参已删——其唯一用途就是喂 ScanConfig,
128
- * u0-data-discovery 偏差 #7)。
129
- *
130
- * 永不抛错——发现本身 fail-safe,单个文件读失败仅记日志。
131
- */
132
- export async function discoverAllAgents(
133
- workspaceRoot: string,
134
- ): Promise<AgentEntry[]> {
135
- return discoverAgents(
136
- workspaceRoot,
137
- getHostServices().discoveryRoots?.()?.agents ?? [],
138
- );
139
- }
140
-
141
- /**
142
- * 注册 session 生命周期 handler,注入 `<available_subagents>` 段。
143
- *
144
- * 三 handler 自管缓存生命周期(不耦合 index.ts session 逻辑):
145
- * - session_start:发现+缓存赋值(fail-safe,异常不阻断,缓存保持 null)
146
- * - before_agent_start:读缓存渲染注入;miss 则 fallback 发现+赋值;空列表/空注入不返回
147
- * systemPrompt;任何异常被吞掉(记日志),不阻断 agent turn
148
- * - session_shutdown:清缓存
149
- *
150
- * pi 支持 async handler,且同一 event 多 handler 链式(前者返回的 systemPrompt 作
151
- * 后者输入)。
72
+ * agent 清单实例:缓存生命周期 / 三 handler 经工厂骨架;装配循环经 assemble 覆写
73
+ * 委托 core discoverAgents(U11 单源,parse/onParseNull 不参与工厂装配——warn 口径
74
+ * 内聚 core:仅「有 frontmatter 但严格校验未通过」才 warn,README 等无 frontmatter
75
+ * 的 .md 静默跳过)。
152
76
  */
153
- export function setupSubagentListInjector(pi: ExtensionAPI): void {
154
- pi.on(
155
- "session_start",
156
- async (_event: SessionStartEvent, ctx: ExtensionContext): Promise<void> => {
157
- try {
158
- setAgentCache(
159
- await discoverAllAgents(
160
- findWorkspaceRoot(ctx.cwd),
161
- ),
162
- );
163
- } catch (err) {
164
- // fail-safe:发现异常不阻断 session,缓存保持 null(before_agent_start 会 fallback)
165
- logger.error("[subagent-list-injector] session_start discover failed", {
166
- reason: err instanceof Error ? err.message : String(err),
167
- });
168
- }
169
- },
170
- );
77
+ const injector = createResourceListInjector<AgentEntry>({
78
+ kind: "agents",
79
+ logTag: "[subagent-list-injector]",
80
+ format: (agents) => formatAgentList(agents, { guide: SUBAGENT_LIST_GUIDE }),
81
+ assemble: (workspaceRoot) =>
82
+ discoverAgents(
83
+ workspaceRoot,
84
+ getHostServices().discoveryRoots?.()?.agents ?? [],
85
+ ),
86
+ });
171
87
 
172
- pi.on(
173
- "before_agent_start",
174
- async (
175
- event: BeforeAgentStartEvent,
176
- ctx: ExtensionContext,
177
- ): Promise<BeforeAgentStartEventResult | void> => {
178
- try {
179
- // 读缓存;miss(session_start 未触发/缓存被清)则 fallback 重新发现+赋值
180
- if (agentCache === null) {
181
- setAgentCache(
182
- await discoverAllAgents(
183
- findWorkspaceRoot(ctx.cwd),
184
- ),
185
- );
186
- }
187
- // agentInjectionCache 与 agentCache 不变量同步(setAgentCache 保证),直接复用
188
- const injection = agentInjectionCache;
189
- if (!injection) return;
190
- return { systemPrompt: event.systemPrompt + injection };
191
- } catch (err) {
192
- logger.error("[subagent-list-injector] before_agent_start failed", {
193
- reason: err instanceof Error ? err.message : String(err),
194
- });
195
- }
196
- },
197
- );
88
+ /** 发现所有可用 agent(装配骨架见工厂;装配算法单源 core discoverAgents,U11)。 */
89
+ export const discoverAllAgents = injector.discover;
198
90
 
199
- pi.on(
200
- "session_shutdown",
201
- (_event: SessionShutdownEvent, _ctx: ExtensionContext): void => {
202
- setAgentCache(null);
203
- },
204
- );
205
- }
91
+ /** 注册 session 生命周期 handler,注入 `<available_subagents>` 段(三 handler 语义见工厂)。 */
92
+ export const setupSubagentListInjector = injector.setup;
@@ -8,69 +8,27 @@
8
8
  * 与 subagent 不对称。补全后模型可直接 `run` 已列出的 workflow,无需先 list。
9
9
  *
10
10
  * 实现要点:
11
- * - 发现走同包 ADR-031 统一发现 discoverResources({kind:"workflows", includeTmp:true})
12
- * (includeTmp 覆盖 .pi/workflows/.tmp/,即 workflow-script generate 的产物)
13
- * - 解析每个 workflow 的 meta 提取 name+description(IF1 parseResourceMeta)
14
- * - description 截断为 prompt 友好的摘要(builtin 的 review-fix-loop 描述超长,全量
15
- * 注入每 turn 会膨胀 prompt)——summarizeDescription C5① 起改用 core barrel
16
- * (算法逐字同本地旧实现)
17
- * - C5①(convergence D-3):formatWorkflowList 下沉 core,本文件改调 core barrel
18
- * (渲染骨架与条目模板逐字节同 pi 旧本地实现——CA2 快照验收前提);guide 文案
19
- * 是 pi 宿主注入(core 不内嵌平台文案)
11
+ * - D7-②(dual-track convergence):同构骨架(缓存对/唯一写点/三 handler)收敛到
12
+ * 同目录 resource-list-injector.ts 工厂,本文件只保留 workflow 侧真差异:
13
+ * - parseWorkflowMeta:.js @pi-meta 块 → WorkflowEntry(m2 收敛改调 shared/meta-parser
14
+ * parseResourceMeta,仅认 @pi-meta 新格式;description 截断用 core summarizeDescription
15
+ * ——builtin 的 review-fix-loop 描述超长,全量注入每 turn 会膨胀 prompt)
16
+ * - includeTmp:发现覆盖 .pi/workflows/.tmp/(workflow-script generate 的产物)
17
+ * - C5①(convergence D-3):formatWorkflowList/summarizeDescription/sortByCodepoint
18
+ * 下沉 core,本文件经 barrel 消费(渲染骨架与条目模板逐字节同 pi 旧本地实现——
19
+ * CA2 快照验收前提);guide 文案是 pi 宿主注入(core 不内嵌平台文案)
20
20
  */
21
21
 
22
22
 
23
23
 
24
- import type {
25
- BeforeAgentStartEvent,
26
- BeforeAgentStartEventResult,
27
- ExtensionAPI,
28
- ExtensionContext,
29
- SessionShutdownEvent,
30
- SessionStartEvent,
31
- } from "@earendil-works/pi-coding-agent";
32
- import { getLogger } from "@zhushanwen/pi-extension-logger";
33
-
34
- import { getHostServices } from "@zhushanwen/subagent-core/core/host-services.ts";
35
- // C5①/C5⑦/C5b:发现与渲染统一走 core barrel——发现链辅助
36
- // (resource-discovery/meta-parser 面:findWorkspaceRoot/getCachedParsed/
37
- // parseResourceMeta)深路径 import 已归零。上方 getHostServices 深路径刻意
38
- // 保留:core 服务定位器(core 内模块统一经它取用宿主端口),barrel 刻意不
39
- // 导出(D5 exports 面即 semver 契约、逐名列出,未列名的内部件不经 barrel——
40
- // 见 subagent-core src/index.ts 头注),壳侧深路径消费经包 `./*` -> src 通配豁免。
41
24
  import {
42
- discoverResources,
43
- findWorkspaceRoot,
44
25
  formatWorkflowList,
45
- getCachedParsed,
46
26
  parseResourceMeta,
47
27
  summarizeDescription,
48
28
  type WorkflowEntry,
49
29
  } from "@zhushanwen/subagent-core";
50
- // U11(sink 设计):注入清单排序码点序单源——深路径消费 core sortByCodepoint
51
- // (与 barrel 版同一实现,深路径遵循任务指定消费形态;`./*` -> src 通配豁免)。
52
- import { sortByCodepoint } from "@zhushanwen/subagent-core/shared/injection-render.ts";
53
-
54
- const logger = getLogger("injector");
55
-
56
- /**
57
- * Session 级 workflow 列表缓存(per-process = per-session),与 agentCache 对称。
58
- * 见 subagent-list-injector.ts 的 agentCache 注释。
59
- */
60
- let workflowCache: WorkflowEntry[] | null = null;
61
30
 
62
- /**
63
- * 注入块渲染缓存(与 workflowCache 同步更新):before_agent_start 每个 turn 都要注入,
64
- * formatWorkflowList 在数据不变时输出完全相同——渲染一次随缓存复用。
65
- */
66
- let workflowInjectionCache: string | null = null;
67
-
68
- /** workflowCache 唯一写点:数据与渲染缓存同步更新(null 清空两者)。 */
69
- function setWorkflowCache(entries: WorkflowEntry[] | null): void {
70
- workflowCache = entries;
71
- workflowInjectionCache =
72
- entries !== null ? formatWorkflowList(entries, { guide: WORKFLOW_LIST_GUIDE }) : null;
73
- }
31
+ import { createResourceListInjector } from "./resource-list-injector.ts";
74
32
 
75
33
  /**
76
34
  * pi 版注入引导文案(C5① guide 参数化——core 渲染函数不内嵌平台文案,宿主注入;
@@ -84,111 +42,26 @@ export const WORKFLOW_LIST_GUIDE =
84
42
 
85
43
  /**
86
44
  * 解析 workflow .js 文件的 meta(name + description),经 IF1 parseResourceMeta。
87
- *
88
- * m2 收敛:删 extractMetaBlock/extractMetaField(本地 brace-match parser),
89
- * 改调 shared/meta-parser.ts parseResourceMeta(统一 parser)。仅认 @pi-meta 新格式。
90
45
  * 投影到 WorkflowEntry {name, description(summarized)} 注入用。
91
46
  */
92
47
  export function parseWorkflowMeta(content: string): WorkflowEntry | null {
93
48
  const meta = parseResourceMeta(content, "workflow");
94
49
  if (!meta || meta.kind !== "workflow") return null;
95
- // path 由 discoverAllWorkflows 从 DiscoveredResource.path 填充
50
+ // path 由工厂 discover 从 DiscoveredResource.path 填充
96
51
  return { name: meta.name, description: summarizeDescription(meta.description), path: "" };
97
52
  }
98
53
 
99
- /**
100
- * 用统一资源发现发现所有可用 workflow(includeTmp 覆盖 generate 产物)。
101
- * 解析经 getCachedParsed mtime 级缓存;输出按 name 码点序排序(KV-cache 契约,
102
- * 见 subagent-list-injector.ts discoverAllAgents 注释)。
103
- * 永不抛错——单文件读失败仅记日志。
104
- */
105
- export async function discoverAllWorkflows(
106
- workspaceRoot: string,
107
- ): Promise<WorkflowEntry[]> {
108
- const resources = await discoverResources({
109
- kind: "workflows",
110
- workspaceRoot,
111
- // 宿主注入根现取(pi 壳 discoveryRoots 每次现取 getAgentDir,实例隔离);
112
- // agentDir 形参已删——其唯一用途就是喂 ScanConfig(u0-data-discovery 偏差 #7)
113
- hostRoots: getHostServices().discoveryRoots?.()?.workflows ?? [],
114
- includeTmp: true,
115
- });
54
+ /** workflow 清单实例:缓存生命周期 / 发现 / 三 handler 全部经工厂骨架。 */
55
+ const injector = createResourceListInjector<WorkflowEntry>({
56
+ kind: "workflows",
57
+ logTag: "[workflow-list-injector]",
58
+ parse: parseWorkflowMeta,
59
+ format: (workflows) => formatWorkflowList(workflows, { guide: WORKFLOW_LIST_GUIDE }),
60
+ includeTmp: true,
61
+ });
116
62
 
117
- const map = new Map<string, WorkflowEntry>();
118
- for (const resource of resources) {
119
- if (!resource.available) continue;
120
- try {
121
- const wf = getCachedParsed(resource.path, parseWorkflowMeta);
122
- if (wf) map.set(wf.name, { ...wf, path: resource.path });
123
- } catch (err) {
124
- logger.error(
125
- `[workflow-list-injector] skip unreadable workflow file ${resource.path}`,
126
- { reason: err instanceof Error ? err.message : String(err) },
127
- );
128
- }
129
- }
130
- // U11:内联码点序 sort 折叠为 core sortByCodepoint(实现逐字等价——同比较器
131
- // 语义:ka < kb ? -1 : ka > kb ? 1 : 0,非变异副本排序)
132
- return sortByCodepoint([...map.values()], (a) => a.name);
133
- }
63
+ /** 用统一资源发现发现所有可用 workflow(includeTmp 覆盖 generate 产物;骨架与排序契约见工厂)。 */
64
+ export const discoverAllWorkflows = injector.discover;
134
65
 
135
- /**
136
- * 注册 session 生命周期 handler,注入 `<available_workflows>` 段。
137
- *
138
- * 与 setupSubagentListInjector 对称:session_start 发现+缓存,before_agent_start
139
- * 读缓存(miss fallback)渲染注入,session_shutdown 清缓存。与 subagent 注入
140
- * handler 链式(pi 串联多 handler 的 systemPrompt 返回值)。
141
- */
142
- export function setupWorkflowListInjector(pi: ExtensionAPI): void {
143
- pi.on(
144
- "session_start",
145
- async (_event: SessionStartEvent, ctx: ExtensionContext): Promise<void> => {
146
- try {
147
- setWorkflowCache(
148
- await discoverAllWorkflows(
149
- findWorkspaceRoot(ctx.cwd),
150
- ),
151
- );
152
- } catch (err) {
153
- // fail-safe:发现异常不阻断 session,缓存保持 null(before_agent_start 会 fallback)
154
- logger.error("[workflow-list-injector] session_start discover failed", {
155
- reason: err instanceof Error ? err.message : String(err),
156
- });
157
- }
158
- },
159
- );
160
-
161
- pi.on(
162
- "before_agent_start",
163
- async (
164
- event: BeforeAgentStartEvent,
165
- ctx: ExtensionContext,
166
- ): Promise<BeforeAgentStartEventResult | void> => {
167
- try {
168
- // 读缓存;miss(session_start 未触发/缓存被清)则 fallback 重新发现+赋值
169
- if (workflowCache === null) {
170
- setWorkflowCache(
171
- await discoverAllWorkflows(
172
- findWorkspaceRoot(ctx.cwd),
173
- ),
174
- );
175
- }
176
- // workflowInjectionCache 与 workflowCache 不变量同步(setWorkflowCache 保证),直接复用
177
- const injection = workflowInjectionCache;
178
- if (!injection) return;
179
- return { systemPrompt: event.systemPrompt + injection };
180
- } catch (err) {
181
- logger.error("[workflow-list-injector] before_agent_start failed", {
182
- reason: err instanceof Error ? err.message : String(err),
183
- });
184
- }
185
- },
186
- );
187
-
188
- pi.on(
189
- "session_shutdown",
190
- (_event: SessionShutdownEvent, _ctx: ExtensionContext): void => {
191
- setWorkflowCache(null);
192
- },
193
- );
194
- }
66
+ /** 注册 session 生命周期 handler,注入 `<available_workflows>` 段(与 subagent 注入 handler 链式)。 */
67
+ export const setupWorkflowListInjector = injector.setup;
@@ -24,7 +24,7 @@ import {
24
24
  findFlattenedArgKeys,
25
25
  } from "@zhushanwen/subagent-core/orchestration/args-meta.ts";
26
26
  import { TOOL_TOP_LEVEL } from "../tool-workflow";
27
- import { parseResourceMeta } from "@zhushanwen/subagent-core/shared/meta-parser.ts";
27
+ import { parseResourceMeta } from "@zhushanwen/subagent-core";
28
28
 
29
29
  // 宿主 reservedKeys 注入(与 tool-workflow ARGS_META_OPTIONS 等值契约)
30
30
  const OPT = { reservedKeys: TOOL_TOP_LEVEL };
@@ -12,39 +12,49 @@
12
12
  // 2. skillPath / cwd 相对路径 → 同步 reject
13
13
  // 3. 合法绝对路径放行 → 参数原样透传 service.execute(守卫零误伤对照)
14
14
  //
15
- // harness 复用 sdk-contract.test.ts 的 mock 链(pi-ai/typebox/subagent-service),
16
- // registerSubagentTool 后捕获 execute 回调直接调用。
15
+ // harness([u-5b / A-V3] 改写):registerSubagentTool 后捕获 execute 回调直接调用。
16
+ // SubagentService 经单例访问器槽注入 fake(setSubagentService,globalThis 槽——
17
+ // executeSubagent 内 getSubagentService() 读同一槽,「守卫拒绝时 service.execute
18
+ // 零触达」的承重断言不依赖整类 mock);pi-ai/typebox 桩改共享桩
19
+ // (src/__tests__/mocks/runtime-stubs.ts,async 工厂 + 动态 import,消费约定见
20
+ // 桩 module 头注)。
21
+
22
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
23
+
24
+ // 共享桩(./mocks/runtime-stubs.ts 相对本文件 = ../../__tests__/mocks/runtime-stubs.ts):
25
+ // vi.mock 工厂提升到文件顶部,体内不能引用普通顶层变量,一律经 async 工厂 +
26
+ // 动态 import 取桩。
27
+ vi.mock("@earendil-works/pi-ai", async () => {
28
+ const { piAiStringEnumStub } = await import("../../__tests__/mocks/runtime-stubs.ts");
29
+ return piAiStringEnumStub();
30
+ });
31
+ vi.mock("typebox", async () => {
32
+ const { typeboxStub } = await import("../../__tests__/mocks/runtime-stubs.ts");
33
+ return typeboxStub;
34
+ });
17
35
 
18
- import { describe, expect, it, vi, beforeEach } from "vitest";
36
+ import { setSubagentService } from "@zhushanwen/subagent-core";
19
37
 
20
- vi.mock("@earendil-works/pi-ai", () => ({
21
- StringEnum: (values: string[]) => ({ type: "string", enum: values }),
22
- }));
23
- vi.mock("typebox", () => ({
24
- Type: {
25
- Object: (props: Record<string, unknown>) => ({ type: "object", properties: props }),
26
- Optional: (schema: unknown) => ({ ...(schema as object), optional: true }),
27
- String: (opts?: Record<string, unknown>) => ({ type: "string", ...opts }),
28
- Boolean: () => ({ type: "boolean" }),
29
- Number: (opts?: Record<string, unknown>) => ({ type: "number", ...opts }),
30
- Array: (items: unknown) => ({ type: "array", items }),
31
- Record: (key: unknown, value: unknown) => ({ type: "object", additionalProperties: value, key }),
32
- Unknown: () => ({ type: "unknown" }),
33
- Union: (members: unknown[]) => ({ type: "union", members }),
34
- Literal: (value: unknown) => ({ type: "literal", value }),
35
- },
36
- }));
38
+ import { registerSubagentTool } from "../../interface/subagent-tool.ts";
39
+ import { mockExtensionApi } from "@zhushanwen/subagent-core/testing/execution/__tests__/helpers/mock-extension-api.ts";
40
+
41
+ // ── 访问器槽注入 helpers ─────────────────────────────────────
37
42
 
38
- // Mock getSubagentService:断言「守卫拒绝时 service.execute 零触达」的承重证据。
43
+ /** fake service:execute 断言面(守卫拒绝 → 零触达)。 */
39
44
  const { mockServiceExecute } = vi.hoisted(() => ({
40
45
  mockServiceExecute: vi.fn(),
41
46
  }));
42
- vi.mock("@zhushanwen/subagent-core/execution/subagent-service.ts", () => ({
43
- getSubagentService: () => ({ execute: mockServiceExecute }),
44
- }));
45
47
 
46
- import { registerSubagentTool } from "../../interface/subagent-tool.ts";
47
- import { mockExtensionApi } from "@zhushanwen/subagent-core/execution/__tests__/helpers/mock-extension-api.ts";
48
+ /** 重置进程级 SubagentService 单例槽(setSubagentService 不接受 null,测试清理用
49
+ * Symbol 直写;key 与生产 getServiceSlot 的 SERVICE_SLOT_KEY 一致)。 */
50
+ function resetServiceSlot(): void {
51
+ const slot = Reflect.get(globalThis, Symbol.for("@zhushanwen/pi-subagents.service")) as
52
+ | { current: unknown }
53
+ | undefined;
54
+ if (slot) slot.current = null;
55
+ }
56
+
57
+ // ── harness ──────────────────────────────────────────────────
48
58
 
49
59
  type ExecuteCb = (...args: unknown[]) => Promise<unknown>;
50
60
 
@@ -82,9 +92,15 @@ describe("subagent tool 路径守卫(skillPath/cwd:绝对路径 + 禁 .. 穿
82
92
  beforeEach(() => {
83
93
  vi.clearAllMocks();
84
94
  mockServiceExecute.mockResolvedValue(stubHandle());
95
+ // 经真实单例访问器注入 fake service(executeSubagent 内 getSubagentService 读同槽)
96
+ setSubagentService({ execute: mockServiceExecute } as never);
85
97
  execute = captureExecute();
86
98
  });
87
99
 
100
+ afterEach(() => {
101
+ resetServiceSlot();
102
+ });
103
+
88
104
  // ── `..` 穿越段拒绝 ──
89
105
 
90
106
  it("skillPath 含 .. 穿越段 → immediate reject,service.execute 零触达", async () => {
@@ -156,9 +156,17 @@ describe("subagent tool runtime handler — 错误文案含纠正正例", () =>
156
156
  // 平铺检测 guard(hasFlattenedStartFields)已删除,源码不应再含此表达式。
157
157
  it("startHandler throw 含 Correct 纠正正例(平铺形态)", () => {
158
158
  // [D6②] startHandler 内核(含 Correct 文案)已下沉 core subagent-actions-core,
159
- // 断言目标跟随文案权威源(resolve 方式同 prompt-quality-batch1 U4 先例)。
159
+ // 断言目标跟随文案权威源。锚点用 ./workflows/* 子入口(exports 映射包根真实目录,
160
+ // 随源码走不依赖 dist 构建——relay-env 锚在 require 条件下落 dist/,CI 不构建
161
+ // workspace 包时 resolve 直接 MODULE_NOT_FOUND)。
162
+ const coreRoot = join(
163
+ dirname(
164
+ createRequire(import.meta.url).resolve("@zhushanwen/subagent-core/workflows/README.md"),
165
+ ),
166
+ "..",
167
+ );
160
168
  const actionsSrc = readFileSync(
161
- createRequire(import.meta.url).resolve("@zhushanwen/subagent-core/execution/subagent-actions-core.ts"),
169
+ join(coreRoot, "src/execution/subagent-actions-core.ts"),
162
170
  "utf-8",
163
171
  );
164
172
  // 四处 throw(input 缺失 / task 空白 / slug 空白 / slug 超长)都应含 Correct 正例。
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import { describe, expect, it, vi } from "vitest";
17
17
 
18
- import { SLUG_MAX_LENGTH } from "@zhushanwen/subagent-core/execution/execute-options-mapper.ts";
18
+ import { SLUG_MAX_LENGTH } from "@zhushanwen/subagent-core";
19
19
  import { REENTRY_BUSY_MESSAGE, type ReentryGuardRef } from "../reentry-guard.ts";
20
20
  import { registerWorkflowTool } from "../tool-workflow.ts";
21
21
 
@@ -22,10 +22,10 @@
22
22
  import type { Component } from "@earendil-works/pi-tui";
23
23
  import type { Theme } from "@earendil-works/pi-coding-agent";
24
24
 
25
- import { displayAgentName } from "@zhushanwen/subagent-core/shared/agent-ref.ts";
26
- import { deriveOutcome } from "@zhushanwen/subagent-core/execution/execution-record.ts";
27
- import { CLOSED_REASONS } from "@zhushanwen/subagent-core/execution/types.ts";
28
- import type { ClosedReason, ExecutionOutcome } from "@zhushanwen/subagent-core/execution/types.ts";
25
+ import { displayAgentName } from "@zhushanwen/subagent-core";
26
+ import { deriveOutcome } from "@zhushanwen/subagent-core";
27
+ import { CLOSED_REASONS } from "@zhushanwen/subagent-core";
28
+ import type { ClosedReason, ExecutionOutcome } from "@zhushanwen/subagent-core";
29
29
  import {
30
30
  firstLineSanitized,
31
31
  padToVisible,