@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.
Files changed (56) hide show
  1. package/README.md +5 -11
  2. package/package.json +8 -11
  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 +61 -6
  6. package/src/index.ts +155 -528
  7. package/src/injectors/__tests__/engine-awareness.test.ts +5 -3
  8. package/src/injectors/__tests__/engine-section-stability.test.ts +48 -33
  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 +80 -8
  13. package/src/injectors/model-list-injector.ts +15 -58
  14. package/src/injectors/resource-list-injector.ts +209 -0
  15. package/src/injectors/subagent-list-injector.ts +53 -224
  16. package/src/injectors/workflow-list-injector.ts +36 -196
  17. package/src/interface/__tests__/detectors.test.ts +46 -35
  18. package/src/interface/__tests__/subagent-tool-path-guard.test.ts +42 -26
  19. package/src/interface/__tests__/subagent-tool-prompt.test.ts +14 -3
  20. package/src/interface/__tests__/tool-workflow-run-builtin-name.test.ts +216 -0
  21. package/src/interface/__tests__/tool-workflow-script-generate.test.ts +17 -6
  22. package/src/interface/__tests__/tool-workflow-throw-paths.test.ts +38 -2
  23. package/src/interface/bg-notify-render.ts +7 -17
  24. package/src/interface/command-actions.ts +5 -15
  25. package/src/interface/commands.ts +116 -76
  26. package/src/interface/format.ts +354 -245
  27. package/src/interface/gui-mappers.ts +18 -22
  28. package/src/interface/helpers.ts +9 -130
  29. package/src/interface/list-component.ts +164 -117
  30. package/src/interface/list-shared.ts +3 -3
  31. package/src/interface/list-view.ts +130 -73
  32. package/src/interface/subagent-actions.ts +50 -675
  33. package/src/interface/subagent-tool-schema.ts +7 -9
  34. package/src/interface/subagent-tool.ts +5 -4
  35. package/src/interface/subagents.ts +9 -8
  36. package/src/interface/tool-render.ts +5 -15
  37. package/src/interface/tool-workflow-script.ts +34 -75
  38. package/src/interface/tool-workflow.ts +62 -96
  39. package/src/interface/tui-kit.ts +305 -0
  40. package/src/interface/views/WorkflowsView.ts +270 -207
  41. package/src/interface/views/__tests__/WorkflowsView-signature.test.ts +3 -3
  42. package/src/interface/views/__tests__/detail-content-session-file.test.ts +7 -4
  43. package/src/interface/views/detail-content.ts +16 -14
  44. package/src/interface/views/view-constants.ts +19 -0
  45. package/src/jsonl-run-store.ts +144 -228
  46. package/agents/analyst.md +0 -61
  47. package/agents/coder.md +0 -70
  48. package/agents/debugger.md +0 -67
  49. package/agents/doc-reviewer.md +0 -50
  50. package/agents/explorer.md +0 -64
  51. package/agents/general-purpose.md +0 -32
  52. package/agents/orchestrator.md +0 -63
  53. package/agents/planner.md +0 -54
  54. package/agents/researcher.md +0 -65
  55. package/agents/reviewer.md +0 -74
  56. package/src/interface/views/format.ts +0 -320
@@ -12,16 +12,17 @@
12
12
  // 编译本 schema 并断言 required/description/enum/pattern 存活——SW 自身测试环境
13
13
  // 把 typebox alias 到 mock(丢 options),SO 侧测试以真实构造为对照基准。
14
14
  //
15
- // 层归属:Interface(工具 schema 的家)。SLUG_MAX_LENGTH 权威定义回归 core 侧
16
- // execution/execute-options-mapper.ts(subagent-core 包抽离 u1-move:core 切面不得
17
- // 反向 import 壳侧 interface,常量随之内化)——此处 re-export 保持既有 import 路径。
15
+ // 层归属:Interface(工具 schema 的家)。SLUG_MAX_LENGTH 权威定义在 core 侧
16
+ // orchestration/models/types.ts([D6 合流迁址] 原 execution/execute-options-mapper.ts
17
+ // 已随 mapper 删除,常量迁至 AgentCallOpts.description 字段同文件;core 切面不得
18
+ // 反向 import 壳侧 interface 的约束不变)——此处 re-export 保持既有 import 路径。
18
19
 
19
20
  import { StringEnum } from "@earendil-works/pi-ai";
20
- import { type Static, Type } from "typebox";
21
+ import { Type } from "typebox";
21
22
 
22
- import { THINKING_ORDER } from "@zhushanwen/subagent-core/shared/model-ref.ts";
23
+ import { THINKING_ORDER } from "@zhushanwen/subagent-core";
23
24
 
24
- import { SLUG_MAX_LENGTH } from "@zhushanwen/subagent-core/execution/execute-options-mapper.ts";
25
+ import { SLUG_MAX_LENGTH } from "@zhushanwen/subagent-core";
25
26
 
26
27
  export { SLUG_MAX_LENGTH };
27
28
 
@@ -158,6 +159,3 @@ export const SubagentParams = Type.Object({
158
159
  })),
159
160
  })),
160
161
  });
161
-
162
- /** Params schema 的 Static 投影(消费方经 `Static<typeof SubagentParams>` 使用,见 subagent-tool.ts)。 */
163
- export type SubagentParamsStatic = Static<typeof SubagentParams>;
@@ -16,13 +16,14 @@ import type { AgentToolResult, ExtensionAPI, ExtensionContext, Theme } from "@ea
16
16
  import { getLogger } from "@zhushanwen/pi-extension-logger";
17
17
  import type { Static } from "typebox";
18
18
 
19
- import { getSubagentService } from "@zhushanwen/subagent-core/execution/subagent-service.ts";
20
- import type { SubagentToolResult } from "@zhushanwen/subagent-core/execution/types.ts";
19
+ import { getSubagentService } from "@zhushanwen/subagent-core";
20
+ import type { SubagentToolResult } from "@zhushanwen/subagent-core";
21
21
  import { extractAgentName } from "./format.ts";
22
22
  import { toGuiCtx } from "./gui-mappers.ts";
23
23
  import { adapter, cancelHandler, closeHandler, forkFromHandler, listHandler, messageHandler, startHandler } from "./subagent-actions.ts";
24
24
  import { SubagentParams } from "./subagent-tool-schema.ts";
25
25
  import { type RenderContext,renderSubagentCall, renderSubagentResult } from "./tool-render.ts";
26
+ import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
26
27
 
27
28
  // ============================================================
28
29
  // 回调类型(抽 alias 绕 registerTool(unknown) 的 TS2307 误报)
@@ -63,7 +64,7 @@ type SubagentRenderResultCb = (
63
64
  // schema 常量经真实 typebox 编译校验(SW 自身 vitest 把 typebox alias 到 mock,
64
65
  // 丢 options——required/description 断言必须以真实构造为基准)。
65
66
 
66
- // extractAgentName 已上移到 ../tui/format.ts 共享(tool-render / subagent-tool 复用)。
67
+ // extractAgentName 已上移到 ./format.ts 共享(tool-render / subagent-tool 复用)。
67
68
 
68
69
  /** exhaustiveness 承重 helper:default 分支把 action 收敛为 never,新增 action 时 tsc 报错。 */
69
70
  function assertNever(value: never): string {
@@ -261,7 +262,7 @@ const subagentRenderCall: SubagentRenderCallCb = (args, theme, ctx) => {
261
262
  // streaming 中间态(partial JSON)或 service 未就绪 → 降级不显示 model(renderCall 不应崩)。
262
263
  // 不阻断渲染,不污染 TUI。开发期开 XYZ_AGENT_DEBUG=1 可写文件日志排查。
263
264
  renderCallLogger.debug("renderCall model resolution failed, degrading", {
264
- reason: err instanceof Error ? err.message : String(err),
265
+ reason: toErrorMessage(err),
265
266
  });
266
267
  }
267
268
  return renderSubagentCall(args, theme, ctx, resolved);
@@ -1,4 +1,4 @@
1
- // src/commands/subagents.ts
1
+ // src/interface/subagents.ts
2
2
  //
3
3
  // /subagents 命令。薄壳——打开 list overlay(等同原 /subagents list [<id>])。
4
4
  //
@@ -9,14 +9,15 @@
9
9
 
10
10
  import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
11
11
 
12
- import { getSubagentService } from "@zhushanwen/subagent-core/execution/subagent-service.ts";
13
- import type { SubagentService } from "@zhushanwen/subagent-core/execution/subagent-service.ts";
14
- import { displayAgentName } from "@zhushanwen/subagent-core/shared/agent-ref.ts";
12
+ import { getSubagentService } from "@zhushanwen/subagent-core";
13
+ import type { SubagentService } from "@zhushanwen/subagent-core";
14
+ import { displayAgentName } from "@zhushanwen/subagent-core";
15
15
  import { messageHandler, startHandler } from "./subagent-actions.ts";
16
16
  import { parseSubagentRpcCommand } from "./command-actions.ts";
17
17
  import type { SubagentRpcAction } from "./command-actions.ts";
18
18
  import { LIST_LIMIT } from "./list-shared.ts";
19
19
  import { createSubagentsView } from "./list-view.ts";
20
+ import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
20
21
 
21
22
  /**
22
23
  * subagent-directive custom_message 的 customType。
@@ -84,7 +85,7 @@ async function rpcCancel(
84
85
  );
85
86
  } catch (err) {
86
87
  // service.cancel 内部 assertReady 在 session_shutdown 并发 dispose 时会抛
87
- const msg = err instanceof Error ? err.message : String(err);
88
+ const msg = toErrorMessage(err);
88
89
  ctx.ui.notify(`Failed to cancel subagent ${recordId}: ${msg}`, "warning");
89
90
  }
90
91
  }
@@ -115,7 +116,7 @@ async function rpcMessage(
115
116
  );
116
117
  ctx.ui.notify(`Message delivered to subagent ${result.slug} (${result.subagentId})`, "info");
117
118
  } catch (err) {
118
- const msg = err instanceof Error ? err.message : String(err);
119
+ const msg = toErrorMessage(err);
119
120
  ctx.ui.notify(`Failed to message subagent ${recordId}: ${msg}`, "warning");
120
121
  }
121
122
  }
@@ -149,7 +150,7 @@ async function rpcStart(
149
150
  );
150
151
  ctx.ui.notify(`Started subagent ${result.slug} (${result.subagentId})`, "info");
151
152
  } catch (err) {
152
- const msg = err instanceof Error ? err.message : String(err);
153
+ const msg = toErrorMessage(err);
153
154
  ctx.ui.notify(`Failed to start subagent ${slug}: ${msg}`, "warning");
154
155
  }
155
156
  }
@@ -230,7 +231,7 @@ export function registerSubagentsCommand(pi: ExtensionAPI): void {
230
231
  if (!service) return null;
231
232
  // collectRecords 合并内存(running) + 磁盘重建 record,按 rootSessionId 过滤。
232
233
  // cancel 只对 running 有效,但全部列出便于用户辨认(终态 record 会被 service 拒绝)。
233
- const records = service.collectRecords(LIST_LIMIT);
234
+ const records = service.queries.collectRecords(LIST_LIMIT);
234
235
  if (records.length === 0) return null;
235
236
  return records.map((r) => ({
236
237
  label: r.id,
@@ -1,4 +1,4 @@
1
- // src/tui/tool-render.ts
1
+ // src/interface/tool-render.ts
2
2
  //
3
3
  // 对话流 tool block 渲染。renderCall(标题行)+ renderResult(背景色 block)。
4
4
  //
@@ -24,13 +24,12 @@ import type { AgentToolResult, Theme } from "@earendil-works/pi-coding-agent";
24
24
  import type {
25
25
  ListResponse,
26
26
  SubagentToolResult,
27
- } from "@zhushanwen/subagent-core/execution/types.ts";
28
- import { displayAgentName } from "@zhushanwen/subagent-core/shared/agent-ref.ts";
27
+ } from "@zhushanwen/subagent-core";
28
+ import { displayAgentName } from "@zhushanwen/subagent-core";
29
29
  import {
30
30
  extractAgentName,
31
- firstLine,
31
+ firstLineSanitized,
32
32
  formatElapsedSeconds,
33
- sanitizeLabel,
34
33
  statusGlyph,
35
34
  type ThemeLike,
36
35
  truncLine,
@@ -304,7 +303,7 @@ function shortSessionLabel(file: string): string {
304
303
  * execute 抛错(如 hub disposed / task 缺失)时,subagents handler 不 catch,
305
304
  * Pi 框架会把 error.message 塞进 result.content[0].text。renderResult 的 fallback
306
305
  * 分支用它把真实原因显示出来,避免只显「no details available」让 AI 盲猜。
307
- * content 可能多行,只取首行(用共享 firstLine 裁断 + sanitize)。
306
+ * content 可能多行,只取首行(共享 firstLineSanitized 裁断 + sanitize)。
308
307
  */
309
308
  function extractResultError(content: AgentToolResult<SubagentToolResult>["content"]): string | undefined {
310
309
  if (!Array.isArray(content)) return undefined;
@@ -322,15 +321,6 @@ function getStringText(item: unknown): string | undefined {
322
321
  return typeof val === "string" && val.trim().length > 0 ? val : undefined;
323
322
  }
324
323
 
325
- /**
326
- * 取文本首个非空行(多行压成首行展示),并 sanitize。
327
- * 用于 done/failed 的交付物预览。
328
- * 共享 firstLine(./format.ts)取首行,本 wrapper 叠加 sanitizeLabel。
329
- */
330
- function firstLineSanitized(text?: string): string {
331
- return sanitizeLabel(firstLine(text));
332
- }
333
-
334
324
  // ============================================================
335
325
  // list 渲染 helper(action:"list" 分支)
336
326
  // ============================================================
@@ -10,14 +10,17 @@
10
10
  * - delete: 删除脚本(前查 isRunning 防删运行中脚本)
11
11
  * - list: 列出可用脚本(调 registry.loadAll)
12
12
  *
13
+ * C5②(convergence D-6):generate 五道闸校验管线 + tmp 写盘下沉 core
14
+ * generateWorkflowScript(报错文案逐字平移,CA2 前提);save/delete 改调 core
15
+ * barrel(第三可选目录参数缺省即 pi 布局 .pi/workflows——pi 现两参调用形态零变化)。
16
+ * 结构化 {ok}|{error} → pi 的 execute-throw 契约转换在本层(pi 只对 execute throw
17
+ * 置 isError:true);AbortSignal 的 aborted 检查留宿主层(C4 偏差 #4 已声明)。
18
+ *
13
19
  * 层归属:Interface。依赖 Pi SDK + engine script-lint + infra workflow-files。
14
20
  *
15
21
  * 参考:domain-models.md §FR-5(tool 收口 4→2)。
16
22
  */
17
23
 
18
- import { mkdirSync, writeFileSync } from "node:fs";
19
- import { resolve as pathResolve } from "node:path";
20
-
21
24
  import { StringEnum } from "@earendil-works/pi-ai";
22
25
  import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
23
26
  import { Text } from "@earendil-works/pi-tui";
@@ -30,12 +33,18 @@ import {
30
33
  guiResult,
31
34
  isGuiCapable,
32
35
  } from "@xyz-agent/extension-protocol";
33
- import type { WorkflowScriptRegistry } from "@zhushanwen/subagent-core/orchestration/models/workflow-script-registry.ts";
34
- import { lintScript } from "@zhushanwen/subagent-core/orchestration/script-lint.ts";
35
- import { deleteWorkflow, saveWorkflow } from "@zhushanwen/subagent-core/orchestration/workflow-files.ts";
36
+ // C5②/C5⑦:创作闭环统一走 core barrel(generateWorkflowScript/saveWorkflow/
37
+ // deleteWorkflow/lintScript 均为 barrel 导出面;深路径在 npm/vendored 形态不可达)
38
+ import {
39
+ deleteWorkflow,
40
+ generateWorkflowScript,
41
+ lintScript,
42
+ saveWorkflow,
43
+ } from "@zhushanwen/subagent-core";
44
+ import type { WorkflowScriptRegistry } from "@zhushanwen/subagent-core";
36
45
  import { toGuiCtx } from "./gui-mappers.ts";
37
- import { renderTextFallback } from "./views/format.ts";
38
- import { parseResourceMetaDetailed } from "@zhushanwen/subagent-core/shared/meta-parser.ts";
46
+ import { renderTextFallback } from "./format.ts";
47
+ import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
39
48
 
40
49
  // ── Parameter schema ─────────────────────────────────────────
41
50
 
@@ -242,73 +251,23 @@ export function registerWorkflowScriptTool(
242
251
 
243
252
  export function actionGenerate(params: ScriptParams, signal: AbortSignal | undefined): TextContent {
244
253
  if (signal?.aborted) {
245
- // throw(W4b):pi 只对 execute throw 置 isError:true(返回值 isError 被丢弃)
254
+ // throw(W4b):pi 只对 execute throw 置 isError:true(返回值 isError 被丢弃)。
255
+ // AbortSignal 是 pi tool 契约层关注——core 管线不含 signal 检查,宿主自留(C4 偏差 #4)
246
256
  throw new Error("Operation aborted before start");
247
257
  }
248
- const name = params.name;
249
- const script = params.script;
250
- if (!name || !script) {
251
- throw new Error("generate requires 'name' and 'script' parameters");
252
- }
253
-
254
- // 1. Reject ESM syntax (Worker runs CJS); 'export const meta' 例外
255
- const stripped = script.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
256
- if (/\bimport\s+(?:type\s+)?[\w{*]/.test(stripped)) {
257
- throw new Error(
258
- "Script uses ESM 'import' syntax. Workflow scripts run in a CJS Worker — use require() instead.",
259
- );
260
- }
261
- const hasExportMeta = /\bexport\s+const\s+meta\s*=/.test(stripped);
262
- const otherExports = stripped.match(/\bexport\s+(?:const|let|var|function|default|\{)/g);
263
- if (otherExports && !hasExportMeta) {
264
- throw new Error(
265
- "Script uses ESM 'export' (non-meta). Use 'const meta = {...}' at top level instead.",
266
- );
258
+ const name = params.name ?? "";
259
+ const script = params.script ?? "";
260
+
261
+ // C5②:五道闸(ESM 拒/meta 必需/agent() 必需/语法/@pi-meta round-trip)+ tmp 写盘
262
+ // 全部在 core generateWorkflowScript(缺省 tmpDir 即 pi 布局 .pi/workflows/.tmp,
263
+ // 相对 cwd resolve——与旧本地实现一致)。结构化 {ok}|{error} → execute-throw 契约
264
+ // 转换在此(报错文案逐字平移自 pi 旧版,含 round-trip 行列信息)。
265
+ const result = generateWorkflowScript(name, script);
266
+ if (!result.ok) {
267
+ throw new Error(result.error);
267
268
  }
268
-
269
- // 2. Validate meta declaration (/* @pi-meta */ new format preferred; legacy const meta accepted during transition — m0)
270
- const hasPiMeta = /\/\*\s*@pi-meta\s*\n/.test(script);
271
- const hasLegacyMeta = script.includes("const meta") || script.includes("export const meta");
272
- if (!hasPiMeta && !hasLegacyMeta) {
273
- throw new Error(
274
- "Script must contain a meta declaration: a /* @pi-meta */ YAML block comment (preferred) or legacy const meta = { ... }. The block has the form: a block comment starting with /* @pi-meta followed by YAML (name/description/phases/parameters?/usage?), closed by */ on its own line.",
275
- );
276
- }
277
-
278
- // 3. Check agent usage
279
- if (!/\bagent\s*\(/.test(stripped)) {
280
- throw new Error(
281
- "Script does not contain any agent() calls. A workflow must call agent() at least once.",
282
- );
283
- }
284
-
285
- // 4. Syntax check (wrap in async IIFE like runtime)
286
- const cjsScript = script.replace(/\bexport\s+const\s+meta\b/, "const meta");
287
- try {
288
- new Function(`(async () => { ${cjsScript} })();`);
289
- } catch (err: unknown) {
290
- const msg = err instanceof Error ? err.message : String(err);
291
- throw new Error(`Syntax error in script: ${msg}`);
292
- }
293
-
294
- // 4b. Round-trip: validate /* @pi-meta */ YAML before writing (v5 §4.7 / ERR4 — report linePos, don't write bad files)
295
- if (hasPiMeta) {
296
- const detailed = parseResourceMetaDetailed(script, "workflow");
297
- if (!detailed.ok) {
298
- const loc = "linePos" in detailed && detailed.linePos
299
- ? ` (line ${detailed.linePos.line}, col ${detailed.linePos.col})`
300
- : "";
301
- throw new Error(
302
- `Generated /* @pi-meta */ YAML cannot be parsed${loc}: ${detailed.error}. Common causes: YAML indent errors, patternProperties regex must use double backslash (\\d not \d), or a stray star-slash inside the YAML body. Fix the meta block and retry.`,
303
- );
304
- }
305
- }
306
-
307
- // 5. Write to .tmp directory
308
- const tmpDir = pathResolve(".pi/workflows/.tmp");
309
- mkdirSync(tmpDir, { recursive: true });
310
- const filePath = pathResolve(tmpDir, `${name}.js`);
311
- writeFileSync(filePath, script, "utf-8");
269
+ // result.ok ⇒ name/script 非空(core 对空入参返回 error)
270
+ const filePath = result.path;
312
271
 
313
272
  return {
314
273
  content: [
@@ -393,7 +352,7 @@ async function actionSave(params: ScriptParams): Promise<TextContent> {
393
352
  } catch (err: unknown) {
394
353
  // throw(W4):pi 只对 execute throw 置 isError:true(返回值里的 isError 被
395
354
  // agent-loop 丢弃,agent-loop.js:453-483)——文案原样进 toolResult。
396
- const msg = err instanceof Error ? err.message : String(err);
355
+ const msg = toErrorMessage(err);
397
356
  throw new Error(`Save failed: ${msg}`);
398
357
  }
399
358
  }
@@ -420,7 +379,7 @@ function actionDelete(
420
379
  };
421
380
  } catch (err: unknown) {
422
381
  // throw(W4):同 save——pi 契约只有 throw 才置 isError:true。
423
- const msg = err instanceof Error ? err.message : String(err);
382
+ const msg = toErrorMessage(err);
424
383
  throw new Error(`Delete failed: ${msg}`);
425
384
  }
426
385
  }
@@ -443,7 +402,7 @@ async function actionList(registry: WorkflowScriptRegistry): Promise<TextContent
443
402
  };
444
403
  } catch (err: unknown) {
445
404
  // throw(W4b):list 失败改 throw(原 return isError 被 pi 丢弃),文案保持
446
- const msg = err instanceof Error ? err.message : String(err);
405
+ const msg = toErrorMessage(err);
447
406
  throw new Error(`List failed: ${msg}`);
448
407
  }
449
408
  }
@@ -31,12 +31,20 @@ import {
31
31
  } from "@xyz-agent/extension-protocol";
32
32
  import { type Static, Type } from "typebox";
33
33
 
34
- import { SLUG_MAX_LENGTH } from "@zhushanwen/subagent-core/execution/execute-options-mapper.ts";
35
- import { THINKING_ORDER } from "@zhushanwen/subagent-core/execution/model-resolver.ts";
36
- import type { LauncherDeps } from "@zhushanwen/subagent-core/orchestration/launcher.ts";
37
- import { abortRun, runWorkflow } from "@zhushanwen/subagent-core/orchestration/lifecycle.ts";
38
- import type { RunStore } from "@zhushanwen/subagent-core/orchestration/models/ports.ts";
39
- import type { WorkflowRun } from "@zhushanwen/subagent-core/orchestration/models/workflow-run.ts";
34
+ import { SLUG_MAX_LENGTH } from "@zhushanwen/subagent-core";
35
+ import { THINKING_ORDER } from "@zhushanwen/subagent-core";
36
+ import type { LauncherDeps } from "@zhushanwen/subagent-core";
37
+ import { abortRun, runWorkflow } from "@zhushanwen/subagent-core";
38
+ import type { RunStore } from "@zhushanwen/subagent-core";
39
+ import type { WorkflowRun } from "@zhushanwen/subagent-core";
40
+ // D9 closure:core args-meta 消费(reservedKeys 注入见下方 ARGS_META_OPTIONS)
41
+ // MAX_TIMER_DELAY_MS:OR-1 消费(barrel 导出,深路径经 u-2c 删通配后 tsc 不可解析)
42
+ import {
43
+ argKeysFromMeta,
44
+ findFlattenedArgKeys,
45
+ MAX_TIMER_DELAY_MS,
46
+ } from "@zhushanwen/subagent-core";
47
+ import { runSummary } from "@zhushanwen/subagent-core";
40
48
  import { mapRunIcon, mapRunStatus, toGuiCtx } from "./gui-mappers.ts";
41
49
  import {
42
50
  acquireReentryGuard,
@@ -44,7 +52,8 @@ import {
44
52
  type ReentryGuardRef,
45
53
  releaseReentryGuard,
46
54
  } from "./reentry-guard.ts";
47
- import { formatElapsed, renderTextFallback } from "./views/format.ts";
55
+ import { formatElapsed, renderTextFallback } from "./format.ts";
56
+ import { toErrorMessage } from "@zhushanwen/pi-ext-guards";
48
57
 
49
58
  // ── Parameter schema ─────────────────────────────────────────
50
59
 
@@ -63,7 +72,7 @@ const WORKFLOW_ACTIONS: readonly WorkflowAction[] = [
63
72
  const WorkflowParams = Type.Object({
64
73
  action: StringEnum(WORKFLOW_ACTIONS, { description: "Workflow action to execute" }),
65
74
  name: Type.Optional(
66
- Type.String({ description: "Workflow ref: absolute path to the .js script (use <location> from <available_workflows>; run action)" }),
75
+ Type.String({ description: "Workflow ref: builtin/saved workflow name from <available_workflows> (C5: run accepts names), or absolute path to the .js script (use <location>; run action)" }),
67
76
  ),
68
77
  slug: Type.Optional(
69
78
  Type.String({
@@ -82,7 +91,7 @@ const WorkflowParams = Type.Object({
82
91
  }),
83
92
  ),
84
93
  tokens: Type.Optional(Type.Number({ description: "Max token budget — ONLY set when user explicitly requests a limit; omit = unlimited (default)" })),
85
- time: Type.Optional(Type.Number({ description: "Max time budget in ms — ONLY set when user explicitly requests a limit; omit = unlimited (default)" })),
94
+ time: Type.Optional(Type.Number({ description: `Max time budget in ms — ONLY set when user explicitly requests a limit; omit = unlimited (default; hard ceiling ${MAX_TIMER_DELAY_MS} ms — larger values fail fast at entry)` })),
86
95
  error: Type.Optional(
87
96
  Type.String({ description: "Error/reason message (optional, used with abort)" }),
88
97
  ),
@@ -105,8 +114,12 @@ const RUNID_SHORT = 8;
105
114
  * tool 自身顶层键(workflow params schema 键)——workflow 参数名与 tool 键撞名时
106
115
  * (如 workflow 声明参数 name),顶层同名键是 tool 参数而非平铺(m6 评审 M-3)。
107
116
  * 未来新增 tool 顶层键需同步此集合。
117
+ *
118
+ * D9 下沉后作为 core args-meta 的 reservedKeys 注入(ArgMetaOptions)——core 缺省
119
+ * 空集,不注入则撞名保护失效(run 调用顶层必有 action/name,会被误判平铺)。
120
+ * export 供 detectors.test 复用同一集合防漂移。
108
121
  */
109
- const TOOL_TOP_LEVEL = new Set([
122
+ export const TOOL_TOP_LEVEL = new Set([
110
123
  "action",
111
124
  "name",
112
125
  "slug",
@@ -124,76 +137,11 @@ const TOOL_TOP_LEVEL = new Set([
124
137
  ]);
125
138
 
126
139
  /**
127
- * 从 workflow 参数 schema 动态构建平铺检测的已知键集(m6:schema 即 SSOT——
128
- * 替代 21 键硬编码 KNOWN_ARG_KEYS,消除与参数定义的漂移面)。
129
- *
130
- * - exact:properties keys(精确匹配)
131
- * - patterns:patternProperties 原样转正则数组(如 /^batch\\d+$/——与旧
132
- * KNOWN_ARG_KEY_PREFIXES 语义一致,自动兼容 \\d{2} 等变体;schema pattern 已是
133
- * 正则源码,直接 new RegExp 即可)
134
- * - 构建时排除 TOOL_TOP_LEVEL(撞名保护)
140
+ * core args-meta 的宿主差异注入项(D9 下沉):reservedKeys = TOOL_TOP_LEVEL。
141
+ * core 版 argKeysFromMeta / findFlattenedArgKeys 缺省 reservedKeys 为空集(core
142
+ * 中性形态),不传此项则失去撞名保护、行为不等值——调用点必须携带(等值契约)。
135
143
  */
136
- export function argKeysFromMeta(
137
- parameters: Record<string, unknown> | undefined,
138
- ): { exact: ReadonlySet<string>; patterns: readonly RegExp[] } {
139
- const exact = new Set<string>();
140
- const patterns: RegExp[] = [];
141
- if (parameters === undefined || parameters === null || typeof parameters !== "object") {
142
- return { exact, patterns };
143
- }
144
- const props = parameters.properties;
145
- if (props !== null && typeof props === "object") {
146
- for (const k of Object.keys(props as Record<string, unknown>)) {
147
- if (!TOOL_TOP_LEVEL.has(k)) exact.add(k);
148
- }
149
- }
150
- const pp = parameters.patternProperties;
151
- if (pp !== null && typeof pp === "object") {
152
- for (const p of Object.keys(pp as Record<string, unknown>)) {
153
- try {
154
- const re = new RegExp(p); // schema pattern 已是正则源码
155
- // S1(m6 exec-review):跳过能命中 tool 顶层键的 pattern——否则
156
- // ^run.*$ 类 pattern 会匹配 runId/name 等 tool 键,合法调用恒误报
157
- if ([...TOOL_TOP_LEVEL].some((tk) => re.test(tk))) continue;
158
- patterns.push(re);
159
- } catch (err) {
160
- // 非法 pattern(schema 校验 m3 已保证合法,双保险)——跳过并记录
161
- logger.warn(`[tool-workflow] patternProperties 非法正则跳过: ${p}`, {
162
- reason: err instanceof Error ? err.message : String(err),
163
- });
164
- }
165
- }
166
- }
167
- return { exact, patterns };
168
- }
169
-
170
- /**
171
- * 检测弱模型把 args 子字段平铺到 workflow params 顶层(P0 静默失败防护)。
172
- * 返回被平铺的键名列表(空 = 未平铺)。export 供 behavioral 测试(trigger/no-trigger/edge)。
173
- * 参数取 unknown 以便测试构造任意对象、并解耦 WorkflowToolParams 的 index-signature 限制。
174
- *
175
- * knownKeys/knownPatterns 由 argKeysFromMeta 动态构建(m6)——匹配谓词:
176
- * knownKeys.has(k) || knownPatterns.some(re => re.test(k))(pattern 自带数字后缀
177
- * 语义——loose startsWith 会误报 batchl/target1);保留 args-排除(顶层 + args
178
- * 内共存不算平铺)。
179
- */
180
- export function findFlattenedArgKeys(
181
- params: unknown,
182
- knownKeys: ReadonlySet<string>,
183
- knownPatterns: readonly RegExp[],
184
- ): string[] {
185
- if (typeof params !== "object" || params === null) return [];
186
- const p = params as Record<string, unknown>;
187
- const args = typeof p.args === "object" && p.args !== null ? p.args : undefined;
188
- const isKnownKey = (k: string) =>
189
- knownKeys.has(k) || knownPatterns.some((re) => re.test(k));
190
- // hasOwnProperty.call 而非 in(原型链——constructor/toString 类参数名不被继承键掩盖)
191
- return Object.keys(p).filter(
192
- (k) =>
193
- isKnownKey(k) &&
194
- !(args !== undefined && Object.prototype.hasOwnProperty.call(args, k)),
195
- );
196
- }
144
+ const ARGS_META_OPTIONS = { reservedKeys: TOOL_TOP_LEVEL };
197
145
 
198
146
  // ── Types ────────────────────────────────────────────────────
199
147
 
@@ -321,7 +269,7 @@ export function registerWorkflowTool(
321
269
  "<available_workflows> (injected each turn). For parameter details, read the <location> " +
322
270
  "script file (script header has @pi-meta parameters + usage + phases). Do NOT use " +
323
271
  "workflow-script generate for patterns already covered by available workflows.",
324
- "run: pass the absolute .js path from <available_workflows> <location> as name, then start in background (no user confirmation needed).",
272
+ "run: pass the workflow ref as name — the listed <name> (builtin/saved workflow) or its <location> absolute .js path from <available_workflows> — then start in background (no user confirmation needed).",
325
273
  "Do NOT poll status after starting — results appear automatically via notifyDone.",
326
274
  "Runs are one-shot: there is no pause/resume — to stop a run early use abort; for a fresh result start a new run.",
327
275
  "Call shapes (JSON): " +
@@ -425,7 +373,17 @@ export async function actionRun(
425
373
  // 顶层(缺 args 嵌套)。args ?? {} 会静默 args={},启动缺参 run 不报错——比 subagent
426
374
  // 平铺事故更严重。m6:先 registry.getPath(动态参数集来源——schema 即 SSOT),
427
375
  // not_found 优先返回;平铺检测报错带 Correct 正例纠正。
428
- const script = await deps.registry.getPath(name);
376
+ //
377
+ // C5③(convergence D-4 pi 半边):name 解析先查内置/已保存 workflow 名
378
+ // (registry.get——内置 chain/parallel/map-reduce/scatter-gather/review-fix-loop 或
379
+ // 用户 project/user 级脚本名,按 tmp>project>user>npm 优先级合并)——内置名命中
380
+ // 直接用内置脚本;未命中再走 getPath(绝对路径,~/ 展开——normalizeRef 既有)。
381
+ // 严格超集:现有路径用法零变化(路径串不会撞 workflow 名——名字是简单标识符),
382
+ // 两者都未命中仍走原 not_found 报错(建议清单不变)。
383
+ let script = await deps.registry.get(name);
384
+ if (!script) {
385
+ script = await deps.registry.getPath(name);
386
+ }
429
387
  // W4c:config-loader 的 toCachedMeta 对不可读/不存在文件返回 available:false 的
430
388
  // stub(非 undefined),仅判 !script 会绕过 not_found → 空 sourceCode 假启动
431
389
  //(W4b verifier 探针实测复现)。与下方 suggestions 分支的 wf.available 过滤口径对齐。
@@ -444,7 +402,10 @@ export async function actionRun(
444
402
 
445
403
  // m6:动态参数集(schema 即 SSOT)→ 平铺检测;无 parameters → 单次 warn + 跳过
446
404
  // (legacy const-meta 类永久无检测——D1 无 adapter 声明)
447
- const { exact: knownKeys, patterns: knownPatterns } = argKeysFromMeta(script.meta.parameters);
405
+ const { exact: knownKeys, patterns: knownPatterns } = argKeysFromMeta(
406
+ script.meta.parameters,
407
+ ARGS_META_OPTIONS,
408
+ );
448
409
  if (knownKeys.size === 0 && knownPatterns.length === 0) {
449
410
  // M-2 显式信号:无参数契约(未声明/解析空)→ 单次 warn——静默退化变显式
450
411
  // (m6 exec-review M1:原实现排除 undefined 与设计相反)
@@ -452,7 +413,9 @@ export async function actionRun(
452
413
  `[tool-workflow] ${script.name}: 未声明参数契约(或解析为空)——平铺检测跳过,args 不校验`,
453
414
  );
454
415
  }
455
- const flattened = findFlattenedArgKeys(params, knownKeys, knownPatterns);
416
+ // core 版接收 meta 内联构建键集(签名与本地版键集三参不同,判定谓词逐字同源)——
417
+ // 键集构建两次(此处 + core 内部)仅为 M-2 空契约信号,成本每 run 一次可忽略
418
+ const flattened = findFlattenedArgKeys(params, script.meta.parameters, ARGS_META_OPTIONS);
456
419
  if (flattened.length > 0) {
457
420
  throw new Error(
458
421
  `Detected ${flattened.join(", ")} at top level — they belong inside 'args'. ` +
@@ -468,6 +431,16 @@ export async function actionRun(
468
431
  const args = params.args ?? {};
469
432
  const tokens = params.tokens;
470
433
  const time = params.time;
434
+ // OR-1 入口 fail-fast(unbounded-wait-audit §7.2 T3①):schema 的 time 是
435
+ // Type.Number 直通(无上界)——超 setTimeout 安全域的值会穿透到 lifecycle 内层
436
+ // 防线(assertSafeTimerDelay),而入口拦截让它永不进入副作用链。错误带合法上限
437
+ // 与实际传入值,LLM 可据消息自纠(clamp 或省略走 unlimited 语义)。
438
+ if (time !== undefined && time > MAX_TIMER_DELAY_MS) {
439
+ throw new Error(
440
+ `time budget ${time} ms exceeds the maximum of ${MAX_TIMER_DELAY_MS} ms (~24.8 days). ` +
441
+ `Retry with a smaller "time", or omit it for unlimited.`,
442
+ );
443
+ }
471
444
 
472
445
  // 构建 RunSpec + 启动(m3:parameters 从 script.meta 拷贝——chokepoint 校验用;
473
446
  // 校验失败 → ArgsValidationError 直接 throw 给 pi(W4:err.message 含 §5.3 指引,
@@ -561,26 +534,19 @@ async function actionLifecycle(
561
534
  };
562
535
  } catch (err) {
563
536
  // throw(W4b):abortRun 失败改 throw(原 return isError 被 pi 丢弃),"Error: " 前缀保持
564
- const msg = err instanceof Error ? err.message : String(err);
537
+ const msg = toErrorMessage(err);
565
538
  throw new Error(`Error: ${msg}`);
566
539
  }
567
540
  }
568
541
 
569
542
  // ── helpers ──────────────────────────────────────────────────
570
543
 
571
- /** WorkflowRun → 摘要(status action 用)。 */
544
+ /** WorkflowRun → 摘要(status action 用):core runSummary 单源投影 + 宿主扩展 stateFile。
545
+ *
546
+ * 字段集不再本地维护(runSummary 双投影分叉的收口点,D8/B5);stateFile 依赖
547
+ * RunStore 实例,属宿主扩展——core 投影刻意不依赖具体 store。 */
572
548
  function toRunSummary(run: WorkflowRun, store: RunStore): RunSummary {
573
- return {
574
- runId: run.runId,
575
- name: run.spec.scriptName,
576
- slug: run.spec.slug,
577
- status: run.state.status,
578
- reason: run.state.reason,
579
- startedAt: run.meta.startedAt,
580
- completedAt: run.meta.completedAt,
581
- error: run.state.error,
582
- stateFile: store.stateFilePath(run.runId),
583
- };
549
+ return { ...runSummary(run), stateFile: store.stateFilePath(run.runId) };
584
550
  }
585
551
 
586
552
  // W4b:原 textResult(text, isError) helper 已删除——23 处错误路径全部改 throw