@zhushanwen/pi-subagent-workflow 8.7.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.
- package/README.md +5 -11
- package/package.json +8 -11
- package/skills/workflow-script-format/SKILL.md +32 -13
- package/src/host/__tests__/pi-host.test.ts +23 -2
- package/src/host/pi-host.ts +61 -6
- package/src/index.ts +155 -528
- package/src/injectors/__tests__/engine-awareness.test.ts +5 -3
- package/src/injectors/__tests__/engine-section-stability.test.ts +48 -33
- package/src/injectors/__tests__/model-list-injector.test.ts +18 -21
- package/src/injectors/__tests__/subagent-list-injector.test.ts +54 -14
- package/src/injectors/__tests__/workflow-list-injector.test.ts +26 -12
- package/src/injectors/engine-awareness.ts +80 -8
- package/src/injectors/model-list-injector.ts +15 -58
- package/src/injectors/resource-list-injector.ts +209 -0
- package/src/injectors/subagent-list-injector.ts +53 -224
- package/src/injectors/workflow-list-injector.ts +36 -196
- package/src/interface/__tests__/detectors.test.ts +46 -35
- package/src/interface/__tests__/subagent-tool-path-guard.test.ts +42 -26
- package/src/interface/__tests__/subagent-tool-prompt.test.ts +14 -3
- package/src/interface/__tests__/tool-workflow-run-builtin-name.test.ts +216 -0
- package/src/interface/__tests__/tool-workflow-script-generate.test.ts +17 -6
- package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +38 -2
- package/src/interface/bg-notify-render.ts +7 -17
- package/src/interface/command-actions.ts +5 -15
- package/src/interface/commands.ts +116 -76
- package/src/interface/format.ts +354 -245
- package/src/interface/gui-mappers.ts +18 -22
- package/src/interface/helpers.ts +9 -130
- package/src/interface/list-component.ts +164 -117
- package/src/interface/list-shared.ts +3 -3
- package/src/interface/list-view.ts +130 -73
- package/src/interface/subagent-actions.ts +50 -675
- package/src/interface/subagent-tool-schema.ts +7 -9
- package/src/interface/subagent-tool.ts +5 -4
- package/src/interface/subagents.ts +9 -8
- package/src/interface/tool-render.ts +5 -15
- package/src/interface/tool-workflow-script.ts +34 -75
- package/src/interface/tool-workflow.ts +62 -96
- package/src/interface/tui-kit.ts +305 -0
- package/src/interface/views/WorkflowsView.ts +270 -207
- package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +3 -3
- package/src/interface/views/__tests__/detail-content-session-file.test.ts +7 -4
- package/src/interface/views/detail-content.ts +16 -14
- package/src/interface/views/view-constants.ts +19 -0
- package/src/jsonl-run-store.ts +144 -228
- package/agents/analyst.md +0 -61
- package/agents/coder.md +0 -70
- package/agents/debugger.md +0 -67
- package/agents/doc-reviewer.md +0 -50
- package/agents/explorer.md +0 -64
- package/agents/general-purpose.md +0 -32
- package/agents/orchestrator.md +0 -63
- package/agents/planner.md +0 -54
- package/agents/researcher.md +0 -65
- package/agents/reviewer.md +0 -74
- package/src/interface/views/format.ts +0 -320
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Workflow List Injector(B2 / P1)
|
|
2
|
+
* Workflow List Injector(B2 / P1;C5① 渲染改接 core)
|
|
3
3
|
*
|
|
4
4
|
* 发现所有可用 workflow(.js/.mjs)并通过 before_agent_start 每 turn 注入
|
|
5
5
|
* `<available_workflows>` 段(name + description),与 subagent 注入段对称。
|
|
@@ -8,220 +8,60 @@
|
|
|
8
8
|
* 与 subagent 不对称。补全后模型可直接 `run` 已列出的 workflow,无需先 list。
|
|
9
9
|
*
|
|
10
10
|
* 实现要点:
|
|
11
|
-
* -
|
|
12
|
-
*
|
|
13
|
-
* -
|
|
14
|
-
* - description
|
|
15
|
-
*
|
|
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 不内嵌平台文案)
|
|
16
20
|
*/
|
|
17
21
|
|
|
18
22
|
|
|
19
23
|
|
|
20
|
-
import type {
|
|
21
|
-
BeforeAgentStartEvent,
|
|
22
|
-
BeforeAgentStartEventResult,
|
|
23
|
-
ExtensionAPI,
|
|
24
|
-
ExtensionContext,
|
|
25
|
-
SessionShutdownEvent,
|
|
26
|
-
SessionStartEvent,
|
|
27
|
-
} from "@earendil-works/pi-coding-agent";
|
|
28
|
-
import { getLogger } from "@zhushanwen/pi-extension-logger";
|
|
29
|
-
|
|
30
|
-
import { getHostServices } from "@zhushanwen/subagent-core/core/host-services.ts";
|
|
31
|
-
|
|
32
24
|
import {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
import { escapeXml, renderXmlSection } from "@zhushanwen/subagent-core/shared/xml-injection.ts";
|
|
39
|
-
|
|
40
|
-
const logger = getLogger("injector");
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Session 级 workflow 列表缓存(per-process = per-session),与 agentCache 对称。
|
|
44
|
-
* 见 subagent-list-injector.ts 的 agentCache 注释。
|
|
45
|
-
*/
|
|
46
|
-
let workflowCache: WorkflowEntry[] | null = null;
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* 注入块渲染缓存(与 workflowCache 同步更新):before_agent_start 每个 turn 都要注入,
|
|
50
|
-
* formatWorkflowList 在数据不变时输出完全相同——渲染一次随缓存复用。
|
|
51
|
-
*/
|
|
52
|
-
let workflowInjectionCache: string | null = null;
|
|
53
|
-
|
|
54
|
-
/** workflowCache 唯一写点:数据与渲染缓存同步更新(null 清空两者)。 */
|
|
55
|
-
function setWorkflowCache(entries: WorkflowEntry[] | null): void {
|
|
56
|
-
workflowCache = entries;
|
|
57
|
-
workflowInjectionCache = entries !== null ? formatWorkflowList(entries) : null;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** 注入段中单个 workflow 的最大描述长度(控制每 turn prompt 体积) */
|
|
61
|
-
const MAX_DESC_LEN = 160;
|
|
25
|
+
formatWorkflowList,
|
|
26
|
+
parseResourceMeta,
|
|
27
|
+
summarizeDescription,
|
|
28
|
+
type WorkflowEntry,
|
|
29
|
+
} from "@zhushanwen/subagent-core";
|
|
62
30
|
|
|
63
|
-
|
|
64
|
-
const DESC_BOUNDARY_MIN_RATIO = 0.4;
|
|
65
|
-
|
|
66
|
-
/** 解析后的 workflow 条目(name + 截断后的 description + agentRef 路径) */
|
|
67
|
-
export interface WorkflowEntry {
|
|
68
|
-
name: string;
|
|
69
|
-
description: string;
|
|
70
|
-
/** workflowRef:脚本 .js 文件的绝对路径(注入段 <location>,模型直接引用) */
|
|
71
|
-
path: string;
|
|
72
|
-
}
|
|
31
|
+
import { createResourceListInjector } from "./resource-list-injector.ts";
|
|
73
32
|
|
|
74
33
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
34
|
+
* pi 版注入引导文案(C5① guide 参数化——core 渲染函数不内嵌平台文案,宿主注入;
|
|
35
|
+
* 文案与改造前本地实现逐字一致——workflow 段 guide 无过期问题)。
|
|
36
|
+
*
|
|
37
|
+
* 引导语与具体 workflow 解耦:不写死内置名(列表本身已含全部 workflow,名字/描述
|
|
38
|
+
* 每 turn 由 @pi-meta 动态注入),只给通用路由指引 + read location 参数指针。
|
|
77
39
|
*/
|
|
78
|
-
export
|
|
79
|
-
|
|
80
|
-
maxLen = MAX_DESC_LEN,
|
|
81
|
-
): string {
|
|
82
|
-
const trimmed = desc.trim();
|
|
83
|
-
if (trimmed.length <= maxLen) return trimmed;
|
|
84
|
-
const slice = trimmed.slice(0, maxLen);
|
|
85
|
-
const boundary = Math.max(
|
|
86
|
-
slice.lastIndexOf("。"),
|
|
87
|
-
slice.lastIndexOf(";"),
|
|
88
|
-
slice.lastIndexOf(";"),
|
|
89
|
-
slice.lastIndexOf(". "),
|
|
90
|
-
);
|
|
91
|
-
// 断点过靠前(< 阈值比例)时不采用,改硬截断保留更多信息
|
|
92
|
-
if (boundary > maxLen * DESC_BOUNDARY_MIN_RATIO) return slice.slice(0, boundary + 1);
|
|
93
|
-
return `${slice}…`;
|
|
94
|
-
}
|
|
40
|
+
export const WORKFLOW_LIST_GUIDE =
|
|
41
|
+
"The following workflows are available. Do NOT call list to discover available workflows — they are listed below; use list only for running state. All listed workflows run directly via action:run — do NOT use workflow-script generate for any listed workflow. For parameter details, read the <location> script file (script header has @pi-meta parameters + usage).";
|
|
95
42
|
|
|
96
43
|
/**
|
|
97
44
|
* 解析 workflow .js 文件的 meta(name + description),经 IF1 parseResourceMeta。
|
|
98
|
-
*
|
|
99
|
-
* m2 收敛:删 extractMetaBlock/extractMetaField(本地 brace-match parser),
|
|
100
|
-
* 改调 shared/meta-parser.ts parseResourceMeta(统一 parser)。仅认 @pi-meta 新格式。
|
|
101
45
|
* 投影到 WorkflowEntry {name, description(summarized)} 注入用。
|
|
102
46
|
*/
|
|
103
47
|
export function parseWorkflowMeta(content: string): WorkflowEntry | null {
|
|
104
48
|
const meta = parseResourceMeta(content, "workflow");
|
|
105
49
|
if (!meta || meta.kind !== "workflow") return null;
|
|
106
|
-
// path
|
|
50
|
+
// path 由工厂 discover 从 DiscoveredResource.path 填充
|
|
107
51
|
return { name: meta.name, description: summarizeDescription(meta.description), path: "" };
|
|
108
52
|
}
|
|
109
53
|
|
|
110
|
-
/**
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
): Promise<WorkflowEntry[]> {
|
|
119
|
-
const resources = await discoverResources({
|
|
120
|
-
kind: "workflows",
|
|
121
|
-
workspaceRoot,
|
|
122
|
-
// 宿主注入根现取(pi 壳 discoveryRoots 每次现取 getAgentDir,实例隔离);
|
|
123
|
-
// agentDir 形参已删——其唯一用途就是喂 ScanConfig(u0-data-discovery 偏差 #7)
|
|
124
|
-
hostRoots: getHostServices().discoveryRoots?.()?.workflows ?? [],
|
|
125
|
-
includeTmp: true,
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
const map = new Map<string, WorkflowEntry>();
|
|
129
|
-
for (const resource of resources) {
|
|
130
|
-
if (!resource.available) continue;
|
|
131
|
-
try {
|
|
132
|
-
const wf = getCachedParsed(resource.path, parseWorkflowMeta);
|
|
133
|
-
if (wf) map.set(wf.name, { ...wf, path: resource.path });
|
|
134
|
-
} catch (err) {
|
|
135
|
-
logger.error(
|
|
136
|
-
`[workflow-list-injector] skip unreadable workflow file ${resource.path}`,
|
|
137
|
-
{ reason: err instanceof Error ? err.message : String(err) },
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
return [...map.values()].sort((a, b) =>
|
|
142
|
-
a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* 将 workflow 列表格式化为 XML 注入段。
|
|
148
|
-
*
|
|
149
|
-
* 引导语对齐 subagent injector:workflow 已在下方列出,模型应直接 run;
|
|
150
|
-
* list 仅用于查询运行态。builtin workflow 点名「run directly」。
|
|
151
|
-
* 空列表返回空串(不注入)。
|
|
152
|
-
*/
|
|
153
|
-
export function formatWorkflowList(workflows: WorkflowEntry[]): string {
|
|
154
|
-
if (workflows.length === 0) return "";
|
|
155
|
-
|
|
156
|
-
const items = workflows.map((wf) =>
|
|
157
|
-
` <workflow><name>${escapeXml(wf.name)}</name><description>${escapeXml(wf.description)}</description><location>${escapeXml(wf.path)}</location></workflow>`,
|
|
158
|
-
);
|
|
159
|
-
return renderXmlSection({
|
|
160
|
-
tag: "available_workflows",
|
|
161
|
-
// 引导语与具体 workflow 解耦:不写死内置名(列表本身已含全部 workflow,
|
|
162
|
-
// 名字/描述每 turn 由 @pi-meta 动态注入),只给通用路由指引 + read location 参数指针。
|
|
163
|
-
guide: "The following workflows are available. Do NOT call list to discover available workflows — they are listed below; use list only for running state. All listed workflows run directly via action:run — do NOT use workflow-script generate for any listed workflow. For parameter details, read the <location> script file (script header has @pi-meta parameters + usage).",
|
|
164
|
-
items,
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* 注册 session 生命周期 handler,注入 `<available_workflows>` 段。
|
|
170
|
-
*
|
|
171
|
-
* 与 setupSubagentListInjector 对称:session_start 发现+缓存,before_agent_start
|
|
172
|
-
* 读缓存(miss fallback)渲染注入,session_shutdown 清缓存。与 subagent 注入
|
|
173
|
-
* handler 链式(pi 串联多 handler 的 systemPrompt 返回值)。
|
|
174
|
-
*/
|
|
175
|
-
export function setupWorkflowListInjector(pi: ExtensionAPI): void {
|
|
176
|
-
pi.on(
|
|
177
|
-
"session_start",
|
|
178
|
-
async (_event: SessionStartEvent, ctx: ExtensionContext): Promise<void> => {
|
|
179
|
-
try {
|
|
180
|
-
setWorkflowCache(
|
|
181
|
-
await discoverAllWorkflows(
|
|
182
|
-
findWorkspaceRoot(ctx.cwd),
|
|
183
|
-
),
|
|
184
|
-
);
|
|
185
|
-
} catch (err) {
|
|
186
|
-
// fail-safe:发现异常不阻断 session,缓存保持 null(before_agent_start 会 fallback)
|
|
187
|
-
logger.error("[workflow-list-injector] session_start discover failed", {
|
|
188
|
-
reason: err instanceof Error ? err.message : String(err),
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
},
|
|
192
|
-
);
|
|
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
|
+
});
|
|
193
62
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
async (
|
|
197
|
-
event: BeforeAgentStartEvent,
|
|
198
|
-
ctx: ExtensionContext,
|
|
199
|
-
): Promise<BeforeAgentStartEventResult | void> => {
|
|
200
|
-
try {
|
|
201
|
-
// 读缓存;miss(session_start 未触发/缓存被清)则 fallback 重新发现+赋值
|
|
202
|
-
if (workflowCache === null) {
|
|
203
|
-
setWorkflowCache(
|
|
204
|
-
await discoverAllWorkflows(
|
|
205
|
-
findWorkspaceRoot(ctx.cwd),
|
|
206
|
-
),
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
// workflowInjectionCache 与 workflowCache 不变量同步(setWorkflowCache 保证),直接复用
|
|
210
|
-
const injection = workflowInjectionCache;
|
|
211
|
-
if (!injection) return;
|
|
212
|
-
return { systemPrompt: event.systemPrompt + injection };
|
|
213
|
-
} catch (err) {
|
|
214
|
-
logger.error("[workflow-list-injector] before_agent_start failed", {
|
|
215
|
-
reason: err instanceof Error ? err.message : String(err),
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
},
|
|
219
|
-
);
|
|
63
|
+
/** 用统一资源发现发现所有可用 workflow(includeTmp 覆盖 generate 产物;骨架与排序契约见工厂)。 */
|
|
64
|
+
export const discoverAllWorkflows = injector.discover;
|
|
220
65
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
(_event: SessionShutdownEvent, _ctx: ExtensionContext): void => {
|
|
224
|
-
setWorkflowCache(null);
|
|
225
|
-
},
|
|
226
|
-
);
|
|
227
|
-
}
|
|
66
|
+
/** 注册 session 生命周期 handler,注入 `<available_workflows>` 段(与 subagent 注入 handler 链式)。 */
|
|
67
|
+
export const setupWorkflowListInjector = injector.setup;
|
|
@@ -17,63 +17,73 @@ import { describe, expect, it } from "vitest";
|
|
|
17
17
|
import { readFileSync } from "node:fs";
|
|
18
18
|
import { join } from "node:path";
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
// D9 下沉后权威实现迁至 core args-meta——宿主差异(TOOL_TOP_LEVEL 撞名保护)经
|
|
21
|
+
// reservedKeys 注入,与 tool-workflow 调用点保持同一契约。
|
|
22
|
+
import {
|
|
23
|
+
argKeysFromMeta,
|
|
24
|
+
findFlattenedArgKeys,
|
|
25
|
+
} from "@zhushanwen/subagent-core/orchestration/args-meta.ts";
|
|
26
|
+
import { TOOL_TOP_LEVEL } from "../tool-workflow";
|
|
27
|
+
import { parseResourceMeta } from "@zhushanwen/subagent-core";
|
|
22
28
|
|
|
23
|
-
//
|
|
24
|
-
|
|
29
|
+
// 宿主 reservedKeys 注入(与 tool-workflow ARGS_META_OPTIONS 等值契约)
|
|
30
|
+
const OPT = { reservedKeys: TOOL_TOP_LEVEL };
|
|
31
|
+
|
|
32
|
+
// 真实参数 meta(@pi-meta parameters——m6 动态数据源,与 TC2 同源防漂移)
|
|
33
|
+
function rflMeta() {
|
|
25
34
|
const src = readFileSync(join(__dirname, "../../../node_modules/@zhushanwen/subagent-core/workflows/review-fix-loop.js"), "utf-8");
|
|
26
35
|
const meta = parseResourceMeta(src, "workflow");
|
|
27
36
|
if (!meta || meta.kind !== "workflow" || !meta.parameters) throw new Error("rfl meta");
|
|
28
|
-
return
|
|
37
|
+
return meta.parameters;
|
|
29
38
|
}
|
|
30
|
-
function
|
|
39
|
+
function chainMeta() {
|
|
31
40
|
const src = readFileSync(join(__dirname, "../../../node_modules/@zhushanwen/subagent-core/workflows/chain.js"), "utf-8");
|
|
32
41
|
const meta = parseResourceMeta(src, "workflow");
|
|
33
42
|
if (!meta || meta.kind !== "workflow" || !meta.parameters) throw new Error("chain meta");
|
|
34
|
-
return
|
|
43
|
+
return meta.parameters;
|
|
35
44
|
}
|
|
36
|
-
const RFL =
|
|
37
|
-
const CHAIN =
|
|
45
|
+
const RFL = rflMeta();
|
|
46
|
+
const CHAIN = chainMeta();
|
|
38
47
|
|
|
39
48
|
describe("findFlattenedArgKeys (workflow args flatten detector — P0)", () => {
|
|
40
49
|
it("TC3a: trigger——chain 参数集平铺 task 被识别;跨 workflow 区分(items 非 chain 参数不触发)", () => {
|
|
41
50
|
expect(
|
|
42
|
-
findFlattenedArgKeys({ action: "run", name: "chain", task: "x" }, CHAIN
|
|
51
|
+
findFlattenedArgKeys({ action: "run", name: "chain", task: "x" }, CHAIN, OPT),
|
|
43
52
|
).toEqual(["task"]);
|
|
44
53
|
// 跨 workflow:items 是 map-reduce 参数非 chain 参数 → 不触发(m6 动态集语义)
|
|
45
54
|
expect(
|
|
46
|
-
findFlattenedArgKeys({ action: "run", name: "chain", items: ["a"] }, CHAIN
|
|
55
|
+
findFlattenedArgKeys({ action: "run", name: "chain", items: ["a"] }, CHAIN, OPT),
|
|
47
56
|
).toEqual([]);
|
|
48
57
|
});
|
|
49
58
|
|
|
50
59
|
it("TC3b: 嵌套 args 不触发", () => {
|
|
51
60
|
expect(
|
|
52
|
-
findFlattenedArgKeys({ action: "run", name: "x", args: { task: "x", items: ["a"] } }, CHAIN
|
|
61
|
+
findFlattenedArgKeys({ action: "run", name: "x", args: { task: "x", items: ["a"] } }, CHAIN, OPT),
|
|
53
62
|
).toEqual([]);
|
|
54
63
|
});
|
|
55
64
|
|
|
56
65
|
it("TC3c: 顶层 + args 内共存不触发(args-排除保留)", () => {
|
|
57
66
|
expect(
|
|
58
|
-
findFlattenedArgKeys({ action: "run", name: "x", args: { task: "x" }, task: "y" }, CHAIN
|
|
67
|
+
findFlattenedArgKeys({ action: "run", name: "x", args: { task: "x" }, task: "y" }, CHAIN, OPT),
|
|
59
68
|
).toEqual([]);
|
|
60
69
|
});
|
|
61
70
|
|
|
62
71
|
it("TC3d: 无已知键不触发;空集不触发", () => {
|
|
63
72
|
expect(
|
|
64
|
-
findFlattenedArgKeys({ action: "run", name: "x", args: {} }, CHAIN
|
|
73
|
+
findFlattenedArgKeys({ action: "run", name: "x", args: {} }, CHAIN, OPT),
|
|
65
74
|
).toEqual([]);
|
|
75
|
+
// meta undefined → 空键集(原「空集不触发」等价形态)
|
|
66
76
|
expect(
|
|
67
|
-
findFlattenedArgKeys({ action: "status" },
|
|
77
|
+
findFlattenedArgKeys({ action: "status" }, undefined),
|
|
68
78
|
).toEqual([]);
|
|
69
79
|
});
|
|
70
80
|
|
|
71
81
|
it("TC3e: review-fix-loop fixAgent 平铺被识别(动态集 exact)", () => {
|
|
72
82
|
expect(
|
|
73
|
-
findFlattenedArgKeys({ action: "run", name: "review-fix-loop", fixAgent: "worker" }, RFL
|
|
83
|
+
findFlattenedArgKeys({ action: "run", name: "review-fix-loop", fixAgent: "worker" }, RFL, OPT),
|
|
74
84
|
).toEqual(["fixAgent"]);
|
|
75
85
|
expect(
|
|
76
|
-
findFlattenedArgKeys({ action: "run", name: "review-fix-loop", args: { fixAgent: "worker" } }, RFL
|
|
86
|
+
findFlattenedArgKeys({ action: "run", name: "review-fix-loop", args: { fixAgent: "worker" } }, RFL, OPT),
|
|
77
87
|
).toEqual([]);
|
|
78
88
|
});
|
|
79
89
|
|
|
@@ -81,27 +91,27 @@ describe("findFlattenedArgKeys (workflow args flatten detector — P0)", () => {
|
|
|
81
91
|
expect(
|
|
82
92
|
findFlattenedArgKeys(
|
|
83
93
|
{ action: "run", name: "review-fix-loop", targetType: "git-diff", target: "main" },
|
|
84
|
-
RFL
|
|
85
|
-
|
|
94
|
+
RFL,
|
|
95
|
+
OPT,
|
|
86
96
|
),
|
|
87
97
|
).toEqual(["targetType", "target"]);
|
|
88
98
|
expect(
|
|
89
99
|
findFlattenedArgKeys(
|
|
90
100
|
{ action: "run", name: "review-fix-loop", batch1: "reviewer", autoCommit: true },
|
|
91
|
-
RFL
|
|
92
|
-
|
|
101
|
+
RFL,
|
|
102
|
+
OPT,
|
|
93
103
|
),
|
|
94
104
|
).toEqual(["batch1", "autoCommit"]);
|
|
95
105
|
expect(
|
|
96
106
|
findFlattenedArgKeys(
|
|
97
107
|
{ action: "run", name: "review-fix-loop", args: { targetType: "git-diff", target: "main" } },
|
|
98
|
-
RFL
|
|
99
|
-
|
|
108
|
+
RFL,
|
|
109
|
+
OPT,
|
|
100
110
|
),
|
|
101
111
|
).toEqual([]);
|
|
102
112
|
// batchl 拼错(非数字后缀)不触发(m6 评审 C-1:pattern 自带数字后缀校验)
|
|
103
113
|
expect(
|
|
104
|
-
findFlattenedArgKeys({ action: "run", name: "x", batchl: "reviewer" }, RFL
|
|
114
|
+
findFlattenedArgKeys({ action: "run", name: "x", batchl: "reviewer" }, RFL, OPT),
|
|
105
115
|
).toEqual([]);
|
|
106
116
|
});
|
|
107
117
|
|
|
@@ -109,42 +119,43 @@ describe("findFlattenedArgKeys (workflow args flatten detector — P0)", () => {
|
|
|
109
119
|
expect(
|
|
110
120
|
findFlattenedArgKeys(
|
|
111
121
|
{ action: "run", name: "review-fix-loop", model: "ds-flash", maxFixAttempts: 3, convergeNewIssues: 2, convergeRounds: 3 },
|
|
112
|
-
RFL
|
|
113
|
-
|
|
122
|
+
RFL,
|
|
123
|
+
OPT,
|
|
114
124
|
),
|
|
115
125
|
).toEqual(["maxFixAttempts", "convergeNewIssues", "convergeRounds"]);
|
|
116
126
|
expect(
|
|
117
127
|
findFlattenedArgKeys(
|
|
118
128
|
{ action: "run", name: "review-fix-loop", args: { model: "ds-flash", maxFixAttempts: 3 }, convergeRounds: 3 },
|
|
119
|
-
RFL
|
|
120
|
-
|
|
129
|
+
RFL,
|
|
130
|
+
OPT,
|
|
121
131
|
),
|
|
122
132
|
).toEqual(["convergeRounds"]);
|
|
123
133
|
});
|
|
124
134
|
|
|
125
135
|
it("TC3h: non-object 输入返回 []", () => {
|
|
126
|
-
expect(findFlattenedArgKeys(null
|
|
127
|
-
expect(findFlattenedArgKeys(undefined
|
|
136
|
+
expect(findFlattenedArgKeys(null)).toEqual([]);
|
|
137
|
+
expect(findFlattenedArgKeys(undefined)).toEqual([]);
|
|
128
138
|
});
|
|
129
139
|
|
|
130
140
|
it("TC3i: tool 顶层键不误报(M-3 回归——合成 parameters 直测 argKeysFromMeta 排除)", () => {
|
|
131
141
|
// 合成 parameters:workflow 声明参数 name(tool 键撞名)——argKeysFromMeta 必须排除
|
|
132
|
-
const
|
|
142
|
+
const synthetic = {
|
|
133
143
|
type: "object",
|
|
134
144
|
properties: { name: { type: "string" }, task: { type: "string" } },
|
|
135
145
|
required: ["name"],
|
|
136
|
-
}
|
|
146
|
+
};
|
|
147
|
+
const keys = argKeysFromMeta(synthetic, OPT);
|
|
137
148
|
expect(keys.exact.has("task")).toBe(true);
|
|
138
149
|
expect(keys.exact.has("name")).toBe(false); // TOOL_TOP_LEVEL 排除(M-3 真回归锁定)
|
|
139
150
|
// 合法调用不误报
|
|
140
151
|
expect(
|
|
141
152
|
findFlattenedArgKeys(
|
|
142
153
|
{ action: "run", name: "mywf", args: { name: "x" } },
|
|
143
|
-
|
|
144
|
-
|
|
154
|
+
synthetic,
|
|
155
|
+
OPT,
|
|
145
156
|
),
|
|
146
157
|
).toEqual([]);
|
|
147
158
|
// 真实 meta 不含 name(探针实测 16 键)——附加验证
|
|
148
|
-
expect(RFL.exact.has("name")).toBe(false);
|
|
159
|
+
expect(argKeysFromMeta(RFL, OPT).exact.has("name")).toBe(false);
|
|
149
160
|
});
|
|
150
161
|
});
|
|
@@ -12,39 +12,49 @@
|
|
|
12
12
|
// 2. skillPath / cwd 相对路径 → 同步 reject
|
|
13
13
|
// 3. 合法绝对路径放行 → 参数原样透传 service.execute(守卫零误伤对照)
|
|
14
14
|
//
|
|
15
|
-
// harness
|
|
16
|
-
//
|
|
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 {
|
|
36
|
+
import { setSubagentService } from "@zhushanwen/subagent-core";
|
|
19
37
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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 () => {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import { readFileSync } from "node:fs";
|
|
13
13
|
import { dirname,join } from "node:path";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { createRequire } from "node:module";
|
|
15
16
|
|
|
16
17
|
import { describe, expect, it } from "vitest";
|
|
17
18
|
|
|
@@ -153,12 +154,22 @@ describe("subagent tool runtime handler — 错误文案含纠正正例", () =>
|
|
|
153
154
|
// 让弱模型撞错后第二次能直接照抄正确形态。
|
|
154
155
|
// 拍平后:startParam envelope 删除,平铺 task/slug 是合法形态;
|
|
155
156
|
// 平铺检测 guard(hasFlattenedStartFields)已删除,源码不应再含此表达式。
|
|
156
|
-
it("
|
|
157
|
+
it("startHandler throw 含 Correct 纠正正例(平铺形态)", () => {
|
|
158
|
+
// [D6②] startHandler 内核(含 Correct 文案)已下沉 core subagent-actions-core,
|
|
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
|
+
);
|
|
157
168
|
const actionsSrc = readFileSync(
|
|
158
|
-
join(
|
|
169
|
+
join(coreRoot, "src/execution/subagent-actions-core.ts"),
|
|
159
170
|
"utf-8",
|
|
160
171
|
);
|
|
161
|
-
//
|
|
172
|
+
// 四处 throw(input 缺失 / task 空白 / slug 空白 / slug 超长)都应含 Correct 正例。
|
|
162
173
|
// 用 occurrences 计数——至少 3 处。
|
|
163
174
|
const occurrences = (actionsSrc.match(/Correct: \{"action":"start"/g) ?? []).length;
|
|
164
175
|
expect(occurrences).toBeGreaterThanOrEqual(3);
|