@zhushanwen/pi-subagent-workflow 9.1.1 → 9.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhushanwen/pi-subagent-workflow",
3
- "version": "9.1.1",
3
+ "version": "9.2.1",
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.",
@@ -62,8 +62,8 @@
62
62
  "@zhushanwen/session-delivery": "1.0.0",
63
63
  "@zhushanwen/pi-ext-guards": "0.4.2",
64
64
  "@zhushanwen/pi-extension-logger": "0.6.1",
65
- "@zhushanwen/pi-notify-ledger-host": "0.2.2",
66
- "@zhushanwen/subagent-core": "1.1.0",
65
+ "@zhushanwen/pi-notify-ledger-host": "0.2.4",
66
+ "@zhushanwen/subagent-core": "1.2.1",
67
67
  "@zhushanwen/pi-subagent-cli": "0.5.0",
68
68
  "@zhushanwen/zcode-subagent-cli": "0.4.4"
69
69
  },
@@ -0,0 +1,147 @@
1
+ // staged-discovery.test.ts — 打包态内置资产**发现结果**门禁(档 2,2026-10-03 B′)
2
+ //
3
+ // 为什么单独建这个文件:本次事故(打包版 `subagents` 批量工具报
4
+ // "Built-in workflow 'fan-out' is not available")的根因是**发现层面**——esbuild
5
+ // 自包含 bundle 下 `require.resolve("@zhushanwen/subagent-core/…")` 必败,staged 布局
6
+ // 又无 node_modules,于是孝在包里的 `workflows/` 无人能扫到。既有门禁全是“文件在不在”
7
+ // 级断言(dry-run import 不调工厂、TC8 只比字节),对本类失效天然失明。
8
+ //
9
+ // 本文件的断言对象是**发现结果**(6 内置 workflow + 10 内置 agent 真的被列出来),
10
+ // 而非“文件存在”:前者才能拦住“路径/产物层”回归。两条通路都被锁:
11
+ // ① 纯函数:bundle 形态模块路径 → scope 根;dev/异常形态 → undefined(不误触发);
12
+ // ② 集成:以合成 bundle 模块路径调 `corePackageNpmRoot()`(resolve 必败 → 回退分支),
13
+ // 再把回退根当 npm 槽根跑**真实** `discoverResources`,断言结果与源目录逐文件一致。
14
+ //
15
+ // 资产集不写死数量(口径:与 `packages/subagent-core/{workflows,agents}` 源目录集合比对,
16
+ // 源增删自动跟随)。
17
+
18
+ import { cpSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
19
+ import { createRequire } from "node:module";
20
+ import { tmpdir } from "node:os";
21
+ import { dirname, join } from "node:path";
22
+ import { pathToFileURL } from "node:url";
23
+
24
+ import { discoverResources, isTargetFile } from "@zhushanwen/subagent-core";
25
+ import { afterAll, describe, expect, it, vi } from "vitest";
26
+
27
+ // pi 宿主协作件 mock:本文件只跑发现链,日志面给可调用桩(回退分支会走 debug)。
28
+ vi.mock("@earendil-works/pi-coding-agent", () => ({
29
+ getAgentDir: () => process.env.PI_CODING_AGENT_DIR ?? "/mock/agent-dir",
30
+ }));
31
+ vi.mock("@zhushanwen/pi-extension-logger", () => ({
32
+ getLogger: () => ({ debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() }),
33
+ }));
34
+
35
+ const { corePackageNpmRoot, stagedScopeRootFromModuleUrl } = await import("../pi-host.ts");
36
+
37
+ /** 本仓 `packages/subagent-core`(源单源:断言口径由此推导,不写死数量)。
38
+ * 路径经与生产同一锚点解析得出(禁止手数 `..` 层级)。 */
39
+ const CORE_PKG = dirname(
40
+ dirname(createRequire(import.meta.url).resolve("@zhushanwen/subagent-core/workflows/README.md")),
41
+ );
42
+
43
+ const tmpDirs: string[] = [];
44
+ function makeTmp(prefix: string): string {
45
+ const dir = mkdtempSync(join(tmpdir(), prefix));
46
+ tmpDirs.push(dir);
47
+ return dir;
48
+ }
49
+ afterAll(() => {
50
+ for (const d of tmpDirs) {
51
+ // teardown 递归删除补 maxRetries:与在途异步写竞争时 ENOTEMPTY 满载 flake
52
+ rmSync(d, { recursive: true, force: true, maxRetries: 5, retryDelay: 20 });
53
+ }
54
+ });
55
+
56
+ /** 合成 staged 布局:<tmp>/@zhushanwen/pi-subagent-workflow/{index.js,workflows,agents}。 */
57
+ function makeStagedLayout(): { scopeDir: string; moduleUrl: string } {
58
+ const root = makeTmp("staged-discovery-");
59
+ const pkgDir = join(root, "@zhushanwen", "pi-subagent-workflow");
60
+ mkdirSync(pkgDir, { recursive: true });
61
+ const indexJs = join(pkgDir, "index.js");
62
+ writeFileSync(indexJs, "// staged bundle placeholder\n", "utf8");
63
+ for (const kind of ["workflows", "agents"] as const) {
64
+ cpSync(join(CORE_PKG, kind), join(pkgDir, kind), { recursive: true });
65
+ }
66
+ return { scopeDir: join(root, "@zhushanwen"), moduleUrl: pathToFileURL(indexJs).href };
67
+ }
68
+
69
+ /** 源目录内可发现文件的集合——判据直接复用发现层导出的 isTargetFile(同一份
70
+ * 函数,发现层口径变化时期望集自动跟随,无双源)。 */
71
+ function sourceExpected(kind: "workflows" | "agents"): string[] {
72
+ return readdirSync(join(CORE_PKG, kind))
73
+ .filter((f) => isTargetFile(f, kind))
74
+ .sort();
75
+ }
76
+
77
+ describe("B′:staged/bundle 形态的 core 资产发现根", () => {
78
+ it("stagedScopeRootFromModuleUrl:bundle 形态(<scope>/<pkg>/index.js)→ 返回 scope 目录", () => {
79
+ const { scopeDir, moduleUrl } = makeStagedLayout();
80
+ expect(stagedScopeRootFromModuleUrl(moduleUrl)).toBe(scopeDir);
81
+ });
82
+
83
+ it("stagedScopeRootFromModuleUrl:dev 形态(非 scope 布局)→ undefined(不误触发回退)", () => {
84
+ const devLike = pathToFileURL(join(CORE_PKG, "workflows", "fan-out.js")).href;
85
+ expect(stagedScopeRootFromModuleUrl(devLike)).toBeUndefined();
86
+ });
87
+
88
+ it("corePackageNpmRoot:dev/workspace 形态走 resolve 通路(不回退)", () => {
89
+ const root = corePackageNpmRoot();
90
+ expect(root).toBeDefined();
91
+ // workspace 形态:scope 根下必须有 subagent-core 包目录(resolve 命中锚点的上三级)
92
+ expect(readdirSync(root as string)).toContain("subagent-core");
93
+ });
94
+
95
+ it("corePackageNpmRoot:bundle 形态 resolve 必败 → 回退到 scope 根", () => {
96
+ const { scopeDir, moduleUrl } = makeStagedLayout();
97
+ expect(corePackageNpmRoot(moduleUrl)).toBe(scopeDir);
98
+ });
99
+ });
100
+
101
+ describe("B′:回退根下的**发现结果**(本批防复发的核心断言)", () => {
102
+ it("workflows:发现结果与源目录集合逐文件一致(内置模板全部可派发)", async () => {
103
+ const { scopeDir } = makeStagedLayout();
104
+ const found = await discoverResources({
105
+ kind: "workflows",
106
+ workspaceRoot: scopeDir,
107
+ hostRoots: [{ dir: scopeDir, source: "npm" }],
108
+ });
109
+ // 只断言**回退根下**的发现结果(发现面还包括真实用户目录 ~/.agents/workflows
110
+ // 等宿主/约定根——本用例的主题是回退根本身,故按路径范围收窄,噪声不介入)
111
+ const names = found
112
+ .filter((r) => r.available && r.path.startsWith(scopeDir))
113
+ .map((r) => r.path.split("/").pop() as string)
114
+ .sort();
115
+ expect(names).toEqual(sourceExpected("workflows"));
116
+ // 回归锚:批量入口的固定执行体必须在列
117
+ expect(names).toContain("fan-out.js");
118
+ });
119
+
120
+ it("agents:发现结果与源目录集合逐文件一致(10 内置角色全部可选)", async () => {
121
+ const { scopeDir } = makeStagedLayout();
122
+ const found = await discoverResources({
123
+ kind: "agents",
124
+ workspaceRoot: scopeDir,
125
+ hostRoots: [{ dir: scopeDir, source: "npm" }],
126
+ });
127
+ const names = found
128
+ .filter((r) => r.available && r.path.startsWith(scopeDir))
129
+ .map((r) => r.path.split("/").pop() as string)
130
+ .sort();
131
+ expect(names).toEqual(sourceExpected("agents"));
132
+ expect(names).toContain("general-purpose.md");
133
+ });
134
+
135
+ it("负向:源资产缺席时发现结果为空(门禁失败方向正确,不会假绿)", async () => {
136
+ const root = makeTmp("staged-discovery-empty-");
137
+ const pkgDir = join(root, "@zhushanwen", "pi-subagent-workflow");
138
+ mkdirSync(pkgDir, { recursive: true });
139
+ writeFileSync(join(pkgDir, "index.js"), "// no assets\n", "utf8");
140
+ const found = await discoverResources({
141
+ kind: "workflows",
142
+ workspaceRoot: join(root, "@zhushanwen"),
143
+ hostRoots: [{ dir: join(root, "@zhushanwen"), source: "npm" }],
144
+ });
145
+ expect(found.filter((r) => r.available)).toEqual([]);
146
+ });
147
+ });
@@ -62,20 +62,34 @@ import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
62
62
  * realpath 去重吸收)。
63
63
  *
64
64
  * 每次调用现解析(不 memo):发现调用点稀疏(session_start + 缓存 miss),解析
65
- * 成本可忽略;失败(异常布局/解析器不可用)降级为不注入并 warn——绝不因资产
66
- * 接线失败阻断发现主链。
65
+ * 成本可忽略;两种形态各有通路,都不阻断发现主链:
66
+ * - **dev / npm 形态**:`require.resolve` 沿本包依赖解析链命中 core 包;
67
+ * - **bundle / staged 打包形态**(2026-10-03 B′ 修复):esbuild 自包含 bundle 把
68
+ * core inline、staged 布局无 node_modules ⇒ resolve 必败(实证:pi-host 日志全天
69
+ * 202 条 'Cannot find module … workflows/README.md',内置 6 workflow + 10 agent
70
+ * 整体不可发现、`subagents` 批量工具报 'Built-in workflow fan-out is not available')。
71
+ * 此时回退到 staged scope 根(见 stagedScopeRootFromModuleUrl)——仅当回退也不
72
+ * 成立(异常布局/解析器不可用)才降级为不注入并 warn。
67
73
  */
68
- function corePackageNpmRoot(): string | undefined {
74
+ export function corePackageNpmRoot(moduleUrl: string = import.meta.url): string | undefined {
69
75
  try {
70
76
  // createRequire 锚定本模块(jiti 加载器下 import.meta.url 可用;不可用则随
71
77
  // catch 降级)。require.resolve 沿 pi-sw 自身的依赖解析链——workspace 与发布态
72
78
  // 都从本包出发命中 core。
73
- const require = createRequire(import.meta.url);
79
+ const require = createRequire(moduleUrl);
74
80
  const anchor = require.resolve("@zhushanwen/subagent-core/workflows/README.md");
75
81
  return dirname(dirname(dirname(anchor)));
76
82
  } catch (err) {
77
- // getLogger 惰性调用(catch 是冷路径——测试环境对 pi-extension-logger 的
78
- // module-level mock 可能返回 undefined,模块级持有会在 import 期踩 undefined)
83
+ const stagedRoot = stagedScopeRootFromModuleUrl(moduleUrl);
84
+ if (stagedRoot !== undefined) {
85
+ // getLogger 惰性调用(冷路径:测试环境对 pi-extension-logger 的 module-level
86
+ // mock 可能返回 undefined,模块级持有会在 import 期踩 undefined)
87
+ getLogger("pi-host").debug(
88
+ "[pi-host] core 包经 staged scope 根注入(bundle 形态,无 node_modules 解析面)",
89
+ { root: stagedRoot },
90
+ );
91
+ return stagedRoot;
92
+ }
79
93
  getLogger("pi-host").warn(
80
94
  "[pi-host] core 包 agents/ 注入根解析失败——10 内置角色可能不可发现",
81
95
  { reason: toErrorMessage(err) },
@@ -84,6 +98,40 @@ function corePackageNpmRoot(): string | undefined {
84
98
  }
85
99
  }
86
100
 
101
+ /**
102
+ * bundle / staged 形态的 core 资产注入根(2026-10-03 B′)。
103
+ *
104
+ * 打包态扩展被 esbuild 打成**自包含 index.js**(core 被 inline),staged 布局内无
105
+ * node_modules ⇒ `require.resolve` 锚点必败。但此时模块位置是
106
+ * `<stagedScope>/<pkg>/index.js`(如
107
+ * `<Resources>/extensions/@zhushanwen/pi-subagent-workflow/index.js`),其**父目录**
108
+ * 正是 `@zhushanwen` scope 目录——npm 槽语义("一级子项 = 包目录")恰好成立,
109
+ * 于是可直接当 npm 槽根用:约定目录扫描即命中
110
+ * `pi-subagent-workflow/{workflows,agents}`(两目录由 `scripts/bundle-extensions.mjs`
111
+ * 随包拷贝;`pi.agents`/`pi.workflows` manifest 约定及其"声明路径不存在 → 整包失败
112
+ * 占位"语义是 subagent-core resource-discovery 的 taiji 自建规则,权威源 =
113
+ * resource-discovery.ts processPackage——pi 上游 manifest 只有
114
+ * extensions/skills/prompts/themes 四字段,无 agents/workflows 概念。已核仓内无任何
115
+ * 包声明该字段,故扫描面内所有包都走约定目录分支,不进 manifest 失败分支)。
116
+ *
117
+ * 与 dev/npm 形态的既有语义一致:npm 槽根扫的就是 scope 下全部包目录(因此本包自带
118
+ * 的 workflows/agents 在 dev/npm 形态同样入发现面),非新增语义。
119
+ *
120
+ * 形态判据 = 父目录名以 `@` 开头:dev(模块在 `extensions/universal/...`)与 npm
121
+ * 安装形态都先走 resolve 成功分支,本回退不可达;仅 bundle/staged 形态命中。
122
+ */
123
+ export function stagedScopeRootFromModuleUrl(moduleUrl: string): string | undefined {
124
+ try {
125
+ const pkgDir = dirname(fileURLToPath(moduleUrl));
126
+ const scopeDir = dirname(pkgDir);
127
+ if (scopeDir === pkgDir) return undefined;
128
+ const scopeName = scopeDir.split(/[\\/]/).pop() ?? "";
129
+ return scopeName.startsWith("@") ? scopeDir : undefined;
130
+ } catch {
131
+ return undefined;
132
+ }
133
+ }
134
+
87
135
  /** agents/workflows 共享的 agentDir 派生根(末级目录名由 kind 决定)。
88
136
  * 顺序与 source 标签逐项对齐 resource-discovery.ts buildScanTargets 的
89
137
  * user-pi → npm → npm-dev 段(根列表按优先级低→高排列,D2 语义边界)。
@@ -243,7 +243,7 @@ describe("formatAgentList CA2 快照(agent guide 壳自有文案唯一字节
243
243
  // guide 段硬编码(锁文本):期望值不插值生产常量——改写 SUBAGENT_LIST_GUIDE
244
244
  // 即红灯,避免「两侧同源插值」下文案漂移无守卫
245
245
  expect(out).toBe(
246
- `\n\n<available_subagents>\nThe 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.\n${expectedItems.join("\n")}\n</available_subagents>`,
246
+ `\n\n<available_subagents>\nThe 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, pass the <location> path from this list as the agent param (usually the right choice); an absolute .md path outside this list also works if the file is a readable agent definition. If no agent matches your task, omit agent (a general-purpose agent is used) and put all role-specific instructions in the task text.\n${expectedItems.join("\n")}\n</available_subagents>`,
247
247
  );
248
248
 
249
249
  // location 前缀替换(资产来源迁移:pi-sw 安装目录 → core 包)后其余字节零变化
@@ -43,7 +43,7 @@ import { createResourceListInjector } from "./resource-list-injector.ts";
43
43
  * 动态指引经 task 文本 / appendSystemPrompt 参数承载。
44
44
  */
45
45
  export const SUBAGENT_LIST_GUIDE =
46
- "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.";
46
+ "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, pass the <location> path from this list as the agent param (usually the right choice); an absolute .md path outside this list also works if the file is a readable agent definition. If no agent matches your task, omit agent (a general-purpose agent is used) and put all role-specific instructions in the task text.";
47
47
 
48
48
  /**
49
49
  * agent 清单实例:缓存生命周期 / 三 handler 经工厂骨架;装配循环经 assemble 覆写
@@ -139,7 +139,11 @@ export function buildGuiComponent(
139
139
  const listResp = input.domain;
140
140
  return guiComponent("list-tree", {
141
141
  items: listResp.response.items.map((it) => ({
142
- label: it.slug ? `${it.agent} · ${it.slug} · ${it.subagentId}` : `${it.agent} · ${it.subagentId}`,
142
+ // GUI 详情层:label 保留完整 agent ref(摘要短名的取舍见 TUI compact/expanded
143
+ // 分层——GUI list-tree 是详情展示,全路径信息优先)
144
+ label: it.slug
145
+ ? `${it.agent} · ${it.slug} · ${it.subagentId}`
146
+ : `${it.agent} · ${it.subagentId}`,
143
147
  status: mapRunStatus(it.status),
144
148
  icon: mapRunIcon(it.status),
145
149
  })),
@@ -200,7 +200,7 @@ describe("renderSubagentResult — expanded 与 compact 的分支关系(字节
200
200
  expect(expanded.join("\n")).toContain("/tmp/sessions/old-abc.jsonl");
201
201
  });
202
202
 
203
- it("list:expanded 在 compact 之上追加 session 行(差异锁定)", () => {
203
+ it("list:expanded agent 用完整 ref、并追加 session 行;compact agent 用 basename 短名(分层锁定)", () => {
204
204
  const details = {
205
205
  action: "list",
206
206
  subagentId: null,
@@ -209,7 +209,7 @@ describe("renderSubagentResult — expanded 与 compact 的分支关系(字节
209
209
  running: 1,
210
210
  items: [{
211
211
  subagentId: "sa-1",
212
- agent: "worker",
212
+ agent: "/Users/x/.agents/agents/worker.md",
213
213
  slug: "scan",
214
214
  status: "running",
215
215
  duration: 12,
@@ -218,10 +218,18 @@ describe("renderSubagentResult — expanded 与 compact 的分支关系(字节
218
218
  },
219
219
  };
220
220
  const { compact, expanded } = renderBoth(details);
221
+ // compact:basename 短名(无目录、无 .md),无 session 行。强断言带边界形态:
222
+ // agent 值以完整 accent 包裹出现(防 "worker" 作为其他字段子串命中弱化回归)
223
+ expect(compact.join("\n")).toMatch(/<accent>worker<\/accent>/);
224
+ expect(compact.join("\n")).not.toContain("/Users/x/.agents/");
225
+ expect(compact.join("\n")).not.toContain(".md");
221
226
  expect(compact.join("\n")).not.toContain("session: ");
227
+ // expanded:agent 完整 ref(详情层)+ session 路径行
228
+ expect(expanded.join("\n")).toContain("/Users/x/.agents/agents/worker.md");
229
+ expect(expanded.join("\n")).toContain("session: ");
222
230
  expect(expanded.join("\n")).toContain("/tmp/sessions/sa-1.jsonl");
223
- expect(compact.join("\n")).not.toContain("/tmp/sessions/sa-1.jsonl");
224
- // compact 的行是 expanded 的前缀(追加不修改既有行)
225
- expect(expanded.slice(0, compact.length)).toEqual(compact);
231
+ // 标题行两态一致(首行),差异只在 item agent 行与追加 session 行
232
+ expect(expanded[0]).toBe(compact[0]);
233
+ expect(expanded.length).toBe(compact.length + 1);
226
234
  });
227
235
  });
@@ -906,3 +906,66 @@ describe("TC3i: 宿主 TOOL_TOP_LEVEL reservedKeys 接线锚(M-3 回归)", (
906
906
  expect(argKeysFromMeta(rflParams(), OPT).exact.has("name")).toBe(false);
907
907
  });
908
908
  });
909
+
910
+ // ══════════════════════════════════════════════════════════════
911
+ // renderCall 标题行(displayWorkflowName 短名 + runId/slug 不受影响)
912
+ // ══════════════════════════════════════════════════════════════
913
+ //
914
+ // 锁 2026-09 display 分层:name 是绝对 .js 路径,标题行只出 basename 短名
915
+ // (displayWorkflowName);runId 截断预览与 slug 后缀形态不变。
916
+
917
+ /** 最小 theme stub(fg/bold 直包文本——断言不依赖真实着色映射)。 */
918
+ function makeRenderTheme(): Record<string, (color: string | ((t: string) => string), text?: string) => string> {
919
+ return {
920
+ fg: (_color, text) => (typeof text === "string" ? `<${_color}>${text}</${_color}>` : ""),
921
+ bold: (text) => (typeof text === "string" ? `<b>${text}</b>` : ""),
922
+ } as never;
923
+ }
924
+
925
+ /** pi-tui Text 反射取构造串(tool-render.test.ts 同款 helper——本文件独立内联)。 */
926
+ function textOf(component: unknown): string {
927
+ const obj = component as Record<string, unknown>;
928
+ for (const key of Object.keys(obj)) {
929
+ const v = obj[key];
930
+ if (typeof v === "string" && v.length > 0) return v;
931
+ }
932
+ return "";
933
+ }
934
+
935
+ describe("renderCall 标题行:displayWorkflowName 短名 + runId/slug 不受影响", () => {
936
+ const tool = captureWorkflowTool(makeDeps(stubRegistry()), { isProcessing: false });
937
+ const theme = makeRenderTheme();
938
+
939
+ it("绝对路径 name → basename 短名(去 .js、去目录)", () => {
940
+ const out = textOf(
941
+ (tool as unknown as { renderCall: (args: Record<string, unknown>, theme: unknown) => unknown })
942
+ .renderCall({ action: "run", name: "/home/u/wf/batch-report.js" }, theme),
943
+ );
944
+ expect(out).toMatch(/batch-report<\/accent>/); // name 落在 accent 段收口前(前缀空格也在段内)
945
+ expect(out).not.toContain("/home/u/wf/");
946
+ expect(out).not.toContain(".js");
947
+ });
948
+
949
+ it("runId / slug 后缀不受短名影响(runId 截断预览 + slug 全文)", () => {
950
+ const out = textOf(
951
+ (tool as unknown as { renderCall: (args: Record<string, unknown>, theme: unknown) => unknown })
952
+ .renderCall(
953
+ { action: "run", name: "/home/u/wf/batch-report.js", slug: "nightly", runId: "wf-run-0123456789" },
954
+ theme,
955
+ ),
956
+ );
957
+ // 短名 + slug 全文在位
958
+ expect(out).toContain("batch-report");
959
+ expect(out).toContain("nightly");
960
+ // runId 只出截断预览(ID_PREVIEW_LENGTH = 8 → "wf-run-0"),不泄漏全文
961
+ expect(out).toContain("wf-run-0");
962
+ expect(out).not.toContain("wf-run-0123456789");
963
+ });
964
+
965
+ it("缺 name(status 等无参 action)不崩、不出短名", () => {
966
+ expect(() =>
967
+ (tool as unknown as { renderCall: (args: Record<string, unknown>, theme: unknown) => unknown })
968
+ .renderCall({ action: "status" }, theme),
969
+ ).not.toThrow();
970
+ });
971
+ });
@@ -55,7 +55,7 @@ export const SubagentParams = Type.Object({
55
55
  maxLength: SLUG_MAX_LENGTH,
56
56
  })),
57
57
  agent: Type.Optional(Type.String({
58
- description: 'Agent ref: absolute path to the agent .md file (use <location> from <available_subagents>). If omitted, defaults to "general-purpose" — a generic agent that inherits the main agent\'s model and project context. Do not invent names — only use paths from the injected list.',
58
+ description: 'Agent ref: absolute path to an agent .md file — usually a <location> from <available_subagents>, but ANY absolute .md path works if the file is a readable agent definition (the list is not required). Relative paths and bare names are rejected. If omitted, defaults to "general-purpose" — a generic agent that inherits the main agent\'s model and project context.',
59
59
  })),
60
60
  model: Type.Optional(Type.String({
61
61
  description: 'Model override in "provider/modelId" format. CASE-SENSITIVE: the string must equal a registry entry exactly, including letter case (e.g. "zai-coding-cn/GLM-5.3-Flash", NOT "zai-coding-cn/glm-5.3-flash"). A non-exact match is rejected immediately with "Did you mean" suggestions — retry with the exact suggested string; the system never auto-corrects your input. Resolution order (top wins): (1) this param, (2) agent .md frontmatter model, (3) the main agent\'s current model (zero-config default). An explicit model (param or frontmatter) that is missing or unauthorized THROWS — there is no silent fallback to the main model. Omit this param to inherit the main model.',
@@ -149,7 +149,7 @@ export function registerSubagentTool(pi: ExtensionAPI): void {
149
149
  pi.registerTool({
150
150
  name: "subagent",
151
151
  label: "Subagent",
152
- promptSnippet: "Delegate to specialized subagents (agentRef = absolute .md path from <available_subagents>)",
152
+ promptSnippet: "Delegate to specialized subagents (agentRef = absolute .md path, e.g. a <location> from <available_subagents>)",
153
153
  description: `Delegate a task to a specialized subagent — when to delegate rather than do it yourself.
154
154
 
155
155
  CRITICAL — executionMode "sequential": multiple \`subagent\` calls in the SAME message run one-after-another. For concurrency, start runs in background and tasks run concurrently in the pool (default maxConcurrent=6).
@@ -230,7 +230,8 @@ function buildCompactLines(d: SubagentToolResult, theme: ThemeLike): string[] {
230
230
  /**
231
231
  * 展开视图行内容生成。除下列两个特例外与 compact 逐字节相同(一次性 block 无
232
232
  * 细节可展开),测试锁字节等价:
233
- * - list:compact 结果之上每 item 追加 sessionFile 路径行(renderListExpanded)
233
+ * - list:每 item agent 用完整 ref(compact 用 basename 短名),并追加 sessionFile 路径行
234
+ * (renderListExpanded)
234
235
  * - fork-from:源文件用完整路径(compact 用 basename 短标签)
235
236
  */
236
237
  function buildExpandedLines(d: SubagentToolResult, theme: ThemeLike): string[] {
@@ -309,8 +310,14 @@ function getStringText(item: unknown): string | undefined {
309
310
  // list 渲染 helper(action:"list" 分支)
310
311
  // ============================================================
311
312
 
312
- /** list compact:标题行 + 每行一个 item 摘要(glyph + agent + slug + mode + status + duration)。 */
313
- function renderListCompact(resp: ListResponse, theme: ThemeLike, width: number): string[] {
313
+ /**
314
+ * list compact:标题行 + 每行一个 item 摘要(glyph + agent + slug + mode + status + duration)。
315
+ *
316
+ * agent 展示分层(同 fork-from 先例:compact 短标签 / expanded 完整路径——摘要行
317
+ * 单行宽度优先,详情行信息完整优先):`shortAgent: false`(expanded)输出完整 ref。
318
+ */
319
+ function renderListCompact(resp: ListResponse, theme: ThemeLike, width: number, opts?: { shortAgent?: boolean }): string[] {
320
+ const shortAgent = opts?.shortAgent ?? true;
314
321
  if (resp.items.length === 0) {
315
322
  return [truncLine(theme.fg("dim", `No subagents (running: ${resp.running})`), width)];
316
323
  }
@@ -323,16 +330,21 @@ function renderListCompact(resp: ListResponse, theme: ThemeLike, width: number):
323
330
  const mode = "bg";
324
331
  // slug 非空时在 agent 后展示(· 分隔),空串时省略。
325
332
  const slugPart = it.slug ? `${theme.fg("dim", " · ")}${theme.fg("accent", it.slug)}` : "";
326
- const line = `${theme.fg(glyph.color, icon)} ${theme.fg("accent", it.agent)}${slugPart}`
333
+ // compact 摘要:basename 短名(同 renderSubagentCall 标题行);expanded 详情:完整 ref
334
+ const agent = shortAgent ? displayAgentName(it.agent) : it.agent;
335
+ const line = `${theme.fg(glyph.color, icon)} ${theme.fg("accent", agent)}${slugPart}`
327
336
  + ` ${theme.fg("dim", `· ${mode} · ${it.status} · ${formatElapsedSeconds(it.duration)}`)}`;
328
337
  lines.push(truncLine(`${STREAM_PREFIX}${line}`, width));
329
338
  }
330
339
  return lines;
331
340
  }
332
341
 
333
- /** list expanded:compact 基础上每 item 追加 sessionFile 路径行。 */
342
+ /**
343
+ * list expanded:compact 基础之上每 item agent 用完整 ref(详情层信息优先),并追加
344
+ * sessionFile 路径行。
345
+ */
334
346
  function renderListExpanded(resp: ListResponse, theme: ThemeLike, width: number): string[] {
335
- const lines = renderListCompact(resp, theme, width);
347
+ const lines = renderListCompact(resp, theme, width, { shortAgent: false });
336
348
  for (const it of resp.items) {
337
349
  if (it.sessionFile) {
338
350
  lines.push(truncLine(`${theme.fg("dim", `${FOOTER_PREFIX}session: `)}${it.sessionFile}`, width));
@@ -82,7 +82,7 @@ const SubagentsParams = Type.Object({
82
82
  }),
83
83
  agents: Type.Optional(Type.String({
84
84
  description:
85
- "Comma-separated absolute paths to agent .md files (use <location> from <available_subagents>). " +
85
+ "Comma-separated absolute paths to agent .md files (usually <location> values from <available_subagents>; arbitrary absolute .md paths also work). " +
86
86
  "One path applies to every member; N paths map one-to-one onto tasks in the same order. " +
87
87
  "When more than one path is given it MUST equal the number of tasks — a mismatch fails the run fast (never a silent persona swap). " +
88
88
  "Omit for the default (general-purpose) executor.",
@@ -30,6 +30,7 @@ import { type Static, Type } from "typebox";
30
30
 
31
31
  import { SLUG_MAX_LENGTH } from "@zhushanwen/subagent-core";
32
32
  import { THINKING_ORDER } from "@zhushanwen/subagent-core";
33
+ import { displayWorkflowName } from "@zhushanwen/subagent-core";
33
34
  import type { LauncherDeps } from "@zhushanwen/subagent-core";
34
35
  import { abortRun, resumeRun, runWorkflow } from "@zhushanwen/subagent-core";
35
36
  import type { ResumeRunOptions } from "@zhushanwen/subagent-core";
@@ -86,7 +87,7 @@ const WORKFLOW_ACTIONS: readonly WorkflowAction[] = [
86
87
  const WorkflowParams = Type.Object({
87
88
  action: StringEnum(WORKFLOW_ACTIONS, { description: "Workflow action to execute" }),
88
89
  name: Type.Optional(
89
- Type.String({ description: "Workflow ref: absolute path to the .js script — use the <location> value from <available_workflows> (bare names are rejected; run action)" }),
90
+ Type.String({ description: "Workflow ref: absolute path to a .js script — normally the <location> of a workflow from <available_workflows>, but ANY absolute .js path works if the file declares `@pi-meta kind: \"workflow\"` (the list is not required). Relative paths and bare names are rejected (run action)" }),
90
91
  ),
91
92
  slug: Type.Optional(
92
93
  Type.String({
@@ -224,7 +225,7 @@ export function registerWorkflowTool(
224
225
  "<available_workflows> (injected each turn). For parameter details, read the <location> " +
225
226
  "script file (script header has @pi-meta parameters + usage + phases). Do NOT use " +
226
227
  "workflow-script generate for patterns already covered by available workflows.",
227
- "run: pass the workflow ref as name — ALWAYS the <location> absolute .js path from <available_workflows> (bare names are rejected with a not-found error listing locations).",
228
+ "run: pass the workflow ref as name — normally the <location> absolute .js path from <available_workflows>, but any absolute .js path with a valid @pi-meta also works (the list is not required). Bare names and relative paths are rejected with a not-found error listing locations.",
228
229
  "DO NOT bash sleep or poll status after starting — results appear automatically via notifyDone.",
229
230
  "Runs are one-shot: there is no pause — to stop a run early use abort; for a fresh result start a new run. " +
230
231
  "resume is ONLY for runs whose status is interrupted (crash or session-switch during execution): it replays " +
@@ -241,7 +242,7 @@ export function registerWorkflowTool(
241
242
  "Model/thinkingLevel: omit by default (inherit main agent's model). Only set model/thinkingLevel when the user explicitly requests a specific model or thinking depth for this run.",
242
243
  "Anti-patterns: Flattening args sub-fields (task/items/...) to the top level — they belong inside args. Calling {\"action\":\"run\"} without name.",
243
244
  "CRITICAL: For orchestration patterns, ALWAYS use action:run with the <location> absolute " +
244
- "path of a listed workflow — NEVER use workflow-script action:generate to recreate patterns " +
245
+ "path of the matching listed workflow — NEVER use workflow-script action:generate to recreate patterns " +
245
246
  "already covered by available workflows. workflow-script generate is ONLY for novel patterns.",
246
247
  ],
247
248
  parameters: WorkflowParams,
@@ -296,7 +297,9 @@ export function registerWorkflowTool(
296
297
 
297
298
  renderCall(args: Record<string, unknown>, theme: Theme, _context?: unknown) {
298
299
  const action = String(args.action ?? "");
299
- const name = args.name ? ` ${String(args.name)}` : "";
300
+ // workflow ref 是绝对路径,标题行显示取 basename 短名(displayWorkflowName,
301
+ // 与 subagent 标题行的 displayAgentName 对称);runId/slug 不受影响。
302
+ const name = args.name ? ` ${displayWorkflowName(String(args.name))}` : "";
300
303
  // run action 可选 slug:在 name 后追加 · slug(accent 色)
301
304
  const slug = optionSlugSuffix(args.slug, theme);
302
305
  const runId = args.runId ? ` ${String(args.runId).slice(0, ID_PREVIEW_LENGTH)}` : "";
@@ -325,7 +328,7 @@ export async function actionRun(
325
328
  const name = params.name;
326
329
  if (!name) {
327
330
  throw new Error(
328
- "run requires 'name' parameter (absolute .js path from <available_workflows> <location>). Correct: {\"action\":\"run\",\"name\":\"<ref>\"}",
331
+ "run requires 'name' parameter (an absolute .js path, e.g. a <location> from <available_workflows>). Correct: {\"action\":\"run\",\"name\":\"<ref>\"}",
329
332
  );
330
333
  }
331
334
  // 弱模型常见误用(P0 静默失败):把 task/items 等 args 子字段平铺到 workflow params